From 0b422093148da00dc846c8f7be011d473bf9035e Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 18 Apr 2011 15:42:36 +0200 Subject: [PATCH 01/35] [REF] osv.py: removed unused variable class_pool. bzr revid: vmt@openerp.com-20110418134236-p5a1wikw10wtplvg --- openerp/osv/osv.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 23e67b00334..222c08db930 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -34,7 +34,6 @@ from openerp.tools.translate import translate module_list = [] module_class_list = {} -class_pool = {} class except_osv(Exception): def __init__(self, name, value, exc_type='warning'): @@ -266,7 +265,6 @@ class osv_base(object): if not hasattr(cls, '_module'): cls._module = module module_class_list.setdefault(cls._module, []).append(cls) - class_pool[cls._name] = cls if module not in module_list: module_list.append(cls._module) return None From 255aaa14d211fb857801d285f2e8b87a2d685a2c Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 18 Apr 2011 16:34:48 +0200 Subject: [PATCH 02/35] [REF] openerp-server: cleaned imports. bzr revid: vmt@openerp.com-20110418143448-bezvdw97ybtlyqhc --- openerp-server | 42 ++++++++++++------------------------------ 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/openerp-server b/openerp-server index 1486c493ff5..b5ad0957737 100755 --- a/openerp-server +++ b/openerp-server @@ -39,6 +39,7 @@ import signal import sys import threading import traceback +import time import openerp.release as release __author__ = release.author @@ -77,8 +78,6 @@ if tools.config['db_user'] == 'postgres': logger.error("Connecting to the database as 'postgres' user is forbidden, as it present major security issues. Shutting down.") sys.exit(1) -import time - #---------------------------------------------------------- # init net service #---------------------------------------------------------- @@ -89,35 +88,20 @@ logger.info('initialising distributed objects services') #--------------------------------------------------------------- import openerp.pooler as pooler -#---------------------------------------------------------- -# import basic modules -#---------------------------------------------------------- -import openerp.osv as osv -import openerp.workflow as workflow -import openerp.report as report -import openerp.service as service - -#---------------------------------------------------------- -# import addons -#---------------------------------------------------------- - -import openerp.addons as addons - #---------------------------------------------------------- # Load and update databases if requested #---------------------------------------------------------- -import openerp.service.http_server as service_http_server +import openerp.service.http_server as http_server +import openerp.service.netrpc_server as netrpc_server if not ( tools.config["stop_after_init"] or \ tools.config["translate_in"] or \ tools.config["translate_out"] ): - service_http_server.init_servers() - service_http_server.init_xmlrpc() - service_http_server.init_static_http() - - import openerp.service.netrpc_server as service_netrpc_server - service_netrpc_server.init_servers() + http_server.init_servers() + http_server.init_xmlrpc() + http_server.init_static_http() + netrpc_server.init_servers() if tools.config['db_name']: for dbname in tools.config['db_name'].split(','): @@ -137,8 +121,6 @@ if tools.config['db_name']: # translation stuff #---------------------------------------------------------- if tools.config["translate_out"]: - import csv - if tools.config["language"]: msg = "language %s" % (tools.config["language"],) else: @@ -175,7 +157,6 @@ if tools.config["translate_in"]: if tools.config["stop_after_init"]: sys.exit(0) - #---------------------------------------------------------- # Launch Servers #---------------------------------------------------------- @@ -186,15 +167,16 @@ SIGNALS = dict( [(getattr(signal, sign), sign) for sign in LST_SIGNALS] ) -netsvc.quit_signals_received = 0 +quit_signals_received = 0 def handler(signum, frame): """ :param signum: the signal number :param frame: the interrupted stack frame or None """ - netsvc.quit_signals_received += 1 - if netsvc.quit_signals_received > 1: + global quit_signals_received + quit_signals_received += 1 + if quit_signals_received > 1: sys.stderr.write("Forced shutdown.\n") os._exit(0) @@ -250,7 +232,7 @@ netsvc.Server.startAll() logger.info('OpenERP server is running, waiting for connections...') -while netsvc.quit_signals_received == 0: +while quit_signals_received == 0: time.sleep(60) quit() From 342f2047cf92b9c6bc4dd670b387826f1bc7da0e Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 18 Apr 2011 17:57:33 +0200 Subject: [PATCH 03/35] [REF] openerp-server: better imports. bzr revid: vmt@openerp.com-20110418155733-gt8shgr0yilo20mw --- openerp-server | 105 +++++++++++++++++------------------- openerp/service/__init__.py | 2 + 2 files changed, 51 insertions(+), 56 deletions(-) diff --git a/openerp-server b/openerp-server index b5ad0957737..a5c81d6593e 100755 --- a/openerp-server +++ b/openerp-server @@ -41,9 +41,9 @@ import threading import traceback import time -import openerp.release as release -__author__ = release.author -__version__ = release.version +import openerp +__author__ = openerp.release.author +__version__ = openerp.release.version if os.name == 'posix': import pwd @@ -54,27 +54,26 @@ if os.name == 'posix': sys.exit(1) #----------------------------------------------------------------------- -# import the tools module so that the commandline parameters are parsed +# parse the command line #----------------------------------------------------------------------- -import openerp.tools as tools -tools.config.parse_config(sys.argv[1:]) +openerp.tools.config.parse_config(sys.argv[1:]) +config = openerp.tools.config #---------------------------------------------------------- # get logger #---------------------------------------------------------- -import openerp.netsvc as netsvc -netsvc.init_logger() +openerp.netsvc.init_logger() logger = logging.getLogger('server') -logger.info("OpenERP version - %s", release.version) -for name, value in [('addons_path', tools.config['addons_path']), - ('database hostname', tools.config['db_host'] or 'localhost'), - ('database port', tools.config['db_port'] or '5432'), - ('database user', tools.config['db_user'])]: +logger.info("OpenERP version - %s", __version__) +for name, value in [('addons_path', config['addons_path']), + ('database hostname', config['db_host'] or 'localhost'), + ('database port', config['db_port'] or '5432'), + ('database user', config['db_user'])]: logger.info("%s - %s", name, value) # Don't allow if the connection to PostgreSQL done by postgres user -if tools.config['db_user'] == 'postgres': +if config['db_user'] == 'postgres': logger.error("Connecting to the database as 'postgres' user is forbidden, as it present major security issues. Shutting down.") sys.exit(1) @@ -83,34 +82,28 @@ if tools.config['db_user'] == 'postgres': #---------------------------------------------------------- logger.info('initialising distributed objects services') -#--------------------------------------------------------------- -# connect to the database and initialize it with base if needed -#--------------------------------------------------------------- -import openerp.pooler as pooler - #---------------------------------------------------------- # Load and update databases if requested #---------------------------------------------------------- -import openerp.service.http_server as http_server -import openerp.service.netrpc_server as netrpc_server - -if not ( tools.config["stop_after_init"] or \ - tools.config["translate_in"] or \ - tools.config["translate_out"] ): +if not ( config["stop_after_init"] or \ + config["translate_in"] or \ + config["translate_out"] ): + http_server = openerp.service.http_server + netrpc_server = openerp.service.netrpc_server http_server.init_servers() http_server.init_xmlrpc() http_server.init_static_http() netrpc_server.init_servers() -if tools.config['db_name']: - for dbname in tools.config['db_name'].split(','): - db,pool = pooler.get_db_and_pool(dbname, update_module=tools.config['init'] or tools.config['update'], pooljobs=False) +if config['db_name']: + for dbname in config['db_name'].split(','): + db, pool = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False) cr = db.cursor() - if tools.config["test_file"]: - logger.info('loading test file %s', tools.config["test_file"]) - tools.convert_yaml_import(cr, 'base', file(tools.config["test_file"]), {}, 'test', True) + if config["test_file"]: + logger.info('loading test file %s', config["test_file"]) + openerp.tools.convert_yaml_import(cr, 'base', file(config["test_file"]), {}, 'test', True) cr.rollback() pool.get('ir.cron')._poolJobs(db.dbname) @@ -120,33 +113,33 @@ if tools.config['db_name']: #---------------------------------------------------------- # translation stuff #---------------------------------------------------------- -if tools.config["translate_out"]: - if tools.config["language"]: - msg = "language %s" % (tools.config["language"],) +if config["translate_out"]: + if config["language"]: + msg = "language %s" % (config["language"],) else: msg = "new language" - logger.info('writing translation file for %s to %s', msg, tools.config["translate_out"]) + logger.info('writing translation file for %s to %s', msg, config["translate_out"]) - fileformat = os.path.splitext(tools.config["translate_out"])[-1][1:].lower() - buf = file(tools.config["translate_out"], "w") - dbname = tools.config['db_name'] - cr = pooler.get_db(dbname).cursor() - tools.trans_export(tools.config["language"], tools.config["translate_modules"] or ["all"], buf, fileformat, cr) + fileformat = os.path.splitext(config["translate_out"])[-1][1:].lower() + buf = file(config["translate_out"], "w") + dbname = config['db_name'] + cr = openerp.pooler.get_db(dbname).cursor() + openerp.tools.trans_export(config["language"], config["translate_modules"] or ["all"], buf, fileformat, cr) cr.close() buf.close() logger.info('translation file written successfully') sys.exit(0) -if tools.config["translate_in"]: - context = {'overwrite': tools.config["overwrite_existing_translations"]} - dbname = tools.config['db_name'] - cr = pooler.get_db(dbname).cursor() - tools.trans_load(cr, - tools.config["translate_in"], - tools.config["language"], +if config["translate_in"]: + context = {'overwrite': config["overwrite_existing_translations"]} + dbname = config['db_name'] + cr = openerp.pooler.get_db(dbname).cursor() + openerp.tools.trans_load(cr, + config["translate_in"], + config["language"], context=context) - tools.trans_update_res_ids(cr) + openerp.tools.trans_update_res_ids(cr) cr.commit() cr.close() sys.exit(0) @@ -154,7 +147,7 @@ if tools.config["translate_in"]: #---------------------------------------------------------------------------------- # if we don't want the server to continue to run after initialization, we quit here #---------------------------------------------------------------------------------- -if tools.config["stop_after_init"]: +if config["stop_after_init"]: sys.exit(0) #---------------------------------------------------------- @@ -201,10 +194,10 @@ if os.name == 'posix': signal.signal(signal.SIGQUIT, dumpstacks) def quit(): - netsvc.Agent.quit() - netsvc.Server.quitAll() - if tools.config['pidfile']: - os.unlink(tools.config['pidfile']) + openerp.netsvc.Agent.quit() + openerp.netsvc.Server.quitAll() + if config['pidfile']: + os.unlink(config['pidfile']) logger = logging.getLogger('shutdown') logger.info("Initiating OpenERP Server shutdown") logger.info("Hit CTRL-C again or send a second signal to immediately terminate the server...") @@ -222,13 +215,13 @@ def quit(): time.sleep(0.05) sys.exit(0) -if tools.config['pidfile']: - fd = open(tools.config['pidfile'], 'w') +if config['pidfile']: + fd = open(config['pidfile'], 'w') pidtext = "%d" % (os.getpid()) fd.write(pidtext) fd.close() -netsvc.Server.startAll() +openerp.netsvc.Server.startAll() logger.info('OpenERP server is running, waiting for connections...') diff --git a/openerp/service/__init__.py b/openerp/service/__init__.py index 529acbec628..a5f28e95678 100644 --- a/openerp/service/__init__.py +++ b/openerp/service/__init__.py @@ -19,6 +19,8 @@ # ############################################################################## +import http_server +import netrpc_server import web_services From 7410d8e4106bd4a1916bb670b9f9f09480900719 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 19 Apr 2011 15:40:16 +0200 Subject: [PATCH 04/35] [REF] config: removed duplicated list of options - the list is still reconstructed from the optparse options. bzr revid: vmt@openerp.com-20110419134016-og6ye1hjrb0j52cl --- openerp/tools/config.py | 165 +++++++++++++++++++--------------------- 1 file changed, 78 insertions(+), 87 deletions(-) diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 6dff227a60e..90fee55fbc2 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -39,67 +39,21 @@ def check_ssl(): class configmanager(object): def __init__(self, fname=None): + # Options not exposed on the command line. Command line options will be added + # from optparse's parser. self.options = { - 'email_from':False, - 'xmlrpc_interface': '', # this will bind the server to all interfaces - 'xmlrpc_port': 8069, - 'netrpc_interface': '', - 'netrpc_port': 8070, - 'xmlrpcs_interface': '', # this will bind the server to all interfaces - 'xmlrpcs_port': 8071, - 'db_host': False, - 'db_port': False, - 'db_name': False, - 'db_user': False, - 'db_password': False, - 'db_maxconn': 64, - 'reportgz': False, - 'netrpc': True, - 'xmlrpc': True, - 'xmlrpcs': True, - 'translate_in': None, - 'translate_out': None, - 'overwrite_existing_translations': False, - 'load_language': None, - 'language': None, - 'pg_path': None, 'admin_passwd': 'admin', 'csv_internal_sep': ',', - 'addons_path': None, - 'root_path': None, - 'debug_mode': False, - 'import_partial': "", - 'pidfile': None, - 'logfile': None, - 'logrotate': True, - 'smtp_server': 'localhost', - 'smtp_user': False, - 'smtp_port':25, - 'smtp_ssl':False, - 'smtp_password': False, - 'stop_after_init': False, # this will stop the server after initialization - 'syslog' : False, - 'log_level': logging.INFO, - 'assert_exit_level': logging.ERROR, # level above which a failed assert will be raised - 'cache_timeout': 100000, 'login_message': False, - 'list_db' : True, - 'timezone' : False, # to override the default TZ - 'test_file' : False, - 'test_report_directory' : False, - 'test_disable' : False, - 'test_commit' : False, - 'static_http_enable': False, - 'static_http_document_root': None, - 'static_http_url_prefix': None, - 'secure_cert_file': 'server.cert', - 'secure_pkey_file': 'server.pkey', 'publisher_warranty_url': 'http://services.openerp.com/publisher-warranty/', - 'osv_memory_count_limit': None, # number of records in each osv_memory virtual table - 'osv_memory_age_limit': 1, # hours + 'reportgz': False, + 'root_path': None, } - self.blacklist_for_save = set(["publisher_warranty_url", "load_language", "root_path"]) + # Not exposed in the configuration file. + self.blacklist_for_save = set( + ['publisher_warranty_url', 'load_language', 'root_path', + 'init', 'save', 'config', 'update']) self.misc = {} self.config_file = fname @@ -122,15 +76,18 @@ class configmanager(object): group.add_option("--without-demo", dest="without_demo", help="disable loading demo data for modules to be installed (comma-separated, use \"all\" for all modules). Requires -d and -i. Default is %default", default=False) - group.add_option("-P", "--import-partial", dest="import_partial", - help="Use this for big data importation, if it crashes you will be able to continue at the current state. Provide a filename to store intermediate importation states.", default=False) + group.add_option("-P", "--import-partial", dest="import_partial", default='', + help="Use this for big data importation, if it crashes you will be able to continue at the current state. Provide a filename to store intermediate importation states.") group.add_option("--pidfile", dest="pidfile", help="file where the server pid will be stored") parser.add_option_group(group) group = optparse.OptionGroup(parser, "XML-RPC Configuration") - group.add_option("--xmlrpc-interface", dest="xmlrpc_interface", help="specify the TCP IP address for the XML-RPC protocol") - group.add_option("--xmlrpc-port", dest="xmlrpc_port", help="specify the TCP port for the XML-RPC protocol", type="int") - group.add_option("--no-xmlrpc", dest="xmlrpc", action="store_false", help="disable the XML-RPC protocol") + group.add_option("--xmlrpc-interface", dest="xmlrpc_interface", default='', + help="Specify the TCP IP address for the XML-RPC protocol. The empty string binds to all interfaces.") + group.add_option("--xmlrpc-port", dest="xmlrpc_port", default=8069, + help="specify the TCP port for the XML-RPC protocol", type="int") + group.add_option("--no-xmlrpc", dest="xmlrpc", action="store_false", default=True, + help="disable the XML-RPC protocol") parser.add_option_group(group) title = "XML-RPC Secure Configuration" @@ -138,18 +95,26 @@ class configmanager(object): title += " (disabled as ssl is unavailable)" group = optparse.OptionGroup(parser, title) - group.add_option("--xmlrpcs-interface", dest="xmlrpcs_interface", help="specify the TCP IP address for the XML-RPC Secure protocol") - group.add_option("--xmlrpcs-port", dest="xmlrpcs_port", help="specify the TCP port for the XML-RPC Secure protocol", type="int") - group.add_option("--no-xmlrpcs", dest="xmlrpcs", action="store_false", help="disable the XML-RPC Secure protocol") - group.add_option("--cert-file", dest="secure_cert_file", help="specify the certificate file for the SSL connection") - group.add_option("--pkey-file", dest="secure_pkey_file", help="specify the private key file for the SSL connection") + group.add_option("--xmlrpcs-interface", dest="xmlrpcs_interface", default='', + help="Specify the TCP IP address for the XML-RPC Secure protocol. The empty string binds to all interfaces.") + group.add_option("--xmlrpcs-port", dest="xmlrpcs_port", default=8071, + help="specify the TCP port for the XML-RPC Secure protocol", type="int") + group.add_option("--no-xmlrpcs", dest="xmlrpcs", action="store_false", default=True, + help="disable the XML-RPC Secure protocol") + group.add_option("--cert-file", dest="secure_cert_file", default='server.cert', + help="specify the certificate file for the SSL connection") + group.add_option("--pkey-file", dest="secure_pkey_file", default='server.pkey', + help="specify the private key file for the SSL connection") parser.add_option_group(group) # NET-RPC group = optparse.OptionGroup(parser, "NET-RPC Configuration") - group.add_option("--netrpc-interface", dest="netrpc_interface", help="specify the TCP IP address for the NETRPC protocol") - group.add_option("--netrpc-port", dest="netrpc_port", help="specify the TCP port for the NETRPC protocol", type="int") - group.add_option("--no-netrpc", dest="netrpc", action="store_false", help="disable the NETRPC protocol") + group.add_option("--netrpc-interface", dest="netrpc_interface", default='', + help="specify the TCP IP address for the NETRPC protocol") + group.add_option("--netrpc-port", dest="netrpc_port", default=8070, + help="specify the TCP port for the NETRPC protocol", type="int") + group.add_option("--no-netrpc", dest="netrpc", action="store_false", default=True, + help="disable the NETRPC protocol") parser.add_option_group(group) # Static HTTP @@ -161,45 +126,60 @@ class configmanager(object): # Testing Group group = optparse.OptionGroup(parser, "Testing Configuration") - group.add_option("--test-file", dest="test_file", help="Launch a YML test file.") - group.add_option("--test-report-directory", dest="test_report_directory", help="If set, will save sample of all reports in this directory.") + group.add_option("--test-file", dest="test_file", default=False, + help="Launch a YML test file.") + group.add_option("--test-report-directory", dest="test_report_directory", default=False, + help="If set, will save sample of all reports in this directory.") group.add_option("--test-disable", action="store_true", dest="test_disable", default=False, help="Disable loading test files.") group.add_option("--test-commit", action="store_true", dest="test_commit", default=False, help="Commit database changes performed by tests.") group.add_option("--assert-exit-level", dest='assert_exit_level', type="choice", choices=self._LOGLEVELS.keys(), - help="specify the level at which a failed assertion will stop the server. Accepted values: %s" % (self._LOGLEVELS.keys(),)) + default='error', + help="specify the level at which a failed assertion will stop the server. Accepted values: %s" % (self._LOGLEVELS.keys(),)) parser.add_option_group(group) # Logging Group group = optparse.OptionGroup(parser, "Logging Configuration") group.add_option("--logfile", dest="logfile", help="file where the server log will be stored") - group.add_option("--no-logrotate", dest="logrotate", action="store_false", + group.add_option("--no-logrotate", dest="logrotate", action="store_false", default=True, help="do not rotate the logfile") group.add_option("--syslog", action="store_true", dest="syslog", default=False, help="Send the log to the syslog server") group.add_option('--log-level', dest='log_level', type='choice', choices=self._LOGLEVELS.keys(), + default='info', help='specify the level of the logging. Accepted values: ' + str(self._LOGLEVELS.keys())) parser.add_option_group(group) # SMTP Group group = optparse.OptionGroup(parser, "SMTP Configuration") - group.add_option('--email-from', dest='email_from', help='specify the SMTP email address for sending email') - group.add_option('--smtp', dest='smtp_server', help='specify the SMTP server for sending email') - group.add_option('--smtp-port', dest='smtp_port', help='specify the SMTP port', type="int") - group.add_option('--smtp-ssl', dest='smtp_ssl', action='store_true', help='specify the SMTP server support SSL or not') - group.add_option('--smtp-user', dest='smtp_user', help='specify the SMTP username for sending email') - group.add_option('--smtp-password', dest='smtp_password', help='specify the SMTP password for sending email') + group.add_option('--email-from', dest='email_from', default=False, + help='specify the SMTP email address for sending email') + group.add_option('--smtp', dest='smtp_server', default='localhost', + help='specify the SMTP server for sending email') + group.add_option('--smtp-port', dest='smtp_port', default=25, + help='specify the SMTP port', type="int") + group.add_option('--smtp-ssl', dest='smtp_ssl', action='store_true', default=False, + help='specify the SMTP server support SSL or not') + group.add_option('--smtp-user', dest='smtp_user', default=False, + help='specify the SMTP username for sending email') + group.add_option('--smtp-password', dest='smtp_password', default=False, + help='specify the SMTP password for sending email') parser.add_option_group(group) group = optparse.OptionGroup(parser, "Database related options") - group.add_option("-d", "--database", dest="db_name", help="specify the database name") - group.add_option("-r", "--db_user", dest="db_user", help="specify the database user name") - group.add_option("-w", "--db_password", dest="db_password", help="specify the database password") + group.add_option("-d", "--database", dest="db_name", default=False, + help="specify the database name") + group.add_option("-r", "--db_user", dest="db_user", default=False, + help="specify the database user name") + group.add_option("-w", "--db_password", dest="db_password", default=False, + help="specify the database password") group.add_option("--pg_path", dest="pg_path", help="specify the pg executable path") - group.add_option("--db_host", dest="db_host", help="specify the database host") - group.add_option("--db_port", dest="db_port", help="specify the database port", type="int") - group.add_option("--db_maxconn", dest="db_maxconn", type='int', + group.add_option("--db_host", dest="db_host", default=False, + help="specify the database host") + group.add_option("--db_port", dest="db_port", default=False, + help="specify the database port", type="int") + group.add_option("--db_maxconn", dest="db_maxconn", type='int', default=64, help="specify the the maximum number of physical connections to posgresql") parser.add_option_group(group) @@ -211,6 +191,7 @@ class configmanager(object): group.add_option('--load-language', dest="load_language", help="specifies the languages for the translations you want to be loaded") group.add_option('-l', "--language", dest="language", + default=None, help="specify the language of the translation file. Use it with --i18n-export or --i18n-import") group.add_option("--i18n-export", dest="translate_out", help="export all sentences to be translated to a CSV file, a PO file or a TGZ archive and exit") @@ -226,17 +207,19 @@ class configmanager(object): parser.add_option_group(group) security = optparse.OptionGroup(parser, 'Security-related options') - security.add_option('--no-database-list', action="store_false", dest='list_db', help="disable the ability to return the list of databases") + security.add_option('--no-database-list', action="store_false", dest='list_db', default=True, + help="disable the ability to return the list of databases") parser.add_option_group(security) # Advanced options group = optparse.OptionGroup(parser, "Advanced options") - group.add_option("--cache-timeout", dest="cache_timeout", + group.add_option("--cache-timeout", dest="cache_timeout", default=100000, help="set the timeout for the cache system", type="int") group.add_option('--debug', dest='debug_mode', action='store_true', default=False, help='enable debug mode') group.add_option("--stop-after-init", action="store_true", dest="stop_after_init", default=False, - help="stop the server after it initializes") - group.add_option("-t", "--timezone", dest="timezone", help="specify reference timezone for the server (e.g. Europe/Brussels") + help="stop the server after its initialization") + group.add_option("-t", "--timezone", dest="timezone", default=False, + help="specify reference timezone for the server (e.g. Europe/Brussels") group.add_option("--osv-memory-count-limit", dest="osv_memory_count_limit", default=False, help="Force a limit on the maximum number of records kept in the virtual " "osv_memory tables. The default is False, which means no count-based limit.", @@ -248,6 +231,14 @@ class configmanager(object): type="float") parser.add_option_group(group) + # Copy all optparse options into self.options. + for group in parser.option_groups: + for option in group.option_list: + if option.default == ('NO', 'DEFAULT'): + self.options[option.dest] = None + else: + self.options[option.dest] = option.default + self.parse_config() def parse_config(self, args=[]): From f96d58c9c6837e6fd51d49e20d86fd4907d9a8d4 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 20 Apr 2011 09:23:46 +0200 Subject: [PATCH 05/35] [REF] pooler: added some comments. bzr revid: vmt@openerp.com-20110420072346-os10xhy9lnvposi2 --- openerp/addons/__init__.py | 8 +++++++- openerp/pooler.py | 11 ++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/openerp/addons/__init__.py b/openerp/addons/__init__.py index f91d66eb7a7..c2c6001e04a 100644 --- a/openerp/addons/__init__.py +++ b/openerp/addons/__init__.py @@ -166,7 +166,13 @@ class Node(Singleton): def get_module_path(module, downloaded=False): - """Return the path of the given module.""" + """Return the path of the given module. + + Search the addons paths and return the first path where the given + module is found. If downloaded is True, return the default addons + path if nothing else is found. + + """ initialize_sys_path() for adp in ad_paths: if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)): diff --git a/openerp/pooler.py b/openerp/pooler.py index 3b9875c18fc..3c1b263ef13 100644 --- a/openerp/pooler.py +++ b/openerp/pooler.py @@ -22,6 +22,7 @@ pool_dic = {} def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False, pooljobs=True): + """Return a database connection and an initialized osv_pool.""" if not status: status={} @@ -33,8 +34,12 @@ def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False, import openerp.addons as addons import openerp.osv.osv as osv_osv pool = osv_osv.osv_pool() - pool_dic[db_name] = pool + # Initializing an osv_pool will call general code which will in turn + # call get_db_and_pool (this function) to obtain the osv_pool begin + # initialized. Make it available in the pool_dic then remove it if + # an exception is raised. + pool_dic[db_name] = pool try: addons.load_modules(db, force_demo, status, update_module) except Exception: @@ -55,12 +60,14 @@ def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False, def restart_pool(db_name, force_demo=False, status=None, update_module=False): + """Delete an existing osv_pool and return a database connection and a newly initialized osv_pool.""" if db_name in pool_dic: del pool_dic[db_name] return get_db_and_pool(db_name, force_demo, status, update_module=update_module) def get_db_only(db_name): + """Return a database connection.""" # ATTENTION: # do not put this import outside this function # sql_db must not be loaded before the logger is initialized. @@ -72,10 +79,12 @@ def get_db_only(db_name): def get_db(db_name): + """Return a database connection. The corresponding osv_pool is initialize.""" return get_db_and_pool(db_name)[0] def get_pool(db_name, force_demo=False, status=None, update_module=False): + """Return an osv_pool.""" pool = get_db_and_pool(db_name, force_demo, status, update_module)[1] return pool From 0e1186dd3d50c797ac0676163e1e1a6a5598584c Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 20 Apr 2011 16:14:07 +0200 Subject: [PATCH 06/35] [REF] openerp.pooler/openerp.sql_db: cleaner dependencies: - openerp.pooler no longer provides get_db_only, which is a provided by sql_db - openerp.sql_db does not rely anymore on netsvc, which is goog as it was making a circular import. The downside is that db_close callers have to clean also the Agent themselves. bzr revid: vmt@openerp.com-20110420141407-au0oanwjc0t15vy5 --- openerp/osv/osv.py | 5 +++-- openerp/pooler.py | 16 +++------------- openerp/service/web_services.py | 2 ++ openerp/sql_db.py | 3 +-- openerp/tools/translate.py | 3 ++- 5 files changed, 11 insertions(+), 18 deletions(-) diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 222c08db930..5d2e457f965 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -26,6 +26,7 @@ import orm import openerp.netsvc as netsvc import openerp.pooler as pooler +import openerp.sql_db as sql_db import copy import logging from psycopg2 import IntegrityError, errorcodes @@ -91,7 +92,7 @@ class object_proxy(netsvc.Service): ids = args[3] else: ids = [] - cr = pooler.get_db_only(dbname).cursor() + cr = sql_db.db_connect(db_name).cursor() return src(obj, cr, uid, ids, context=(ctx or {})) except Exception: pass @@ -102,7 +103,7 @@ class object_proxy(netsvc.Service): # be returned, it is the best we have. try: - cr = pooler.get_db_only(dbname).cursor() + cr = sql_db.db_connect(db_name).cursor() res = translate(cr, name=False, source_type=ttype, lang=lang, source=src) if res: diff --git a/openerp/pooler.py b/openerp/pooler.py index 3c1b263ef13..0de5f832494 100644 --- a/openerp/pooler.py +++ b/openerp/pooler.py @@ -19,6 +19,8 @@ # ############################################################################## +import sql_db + pool_dic = {} def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False, pooljobs=True): @@ -26,7 +28,7 @@ def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False, if not status: status={} - db = get_db_only(db_name) + db = sql_db.db_connect(db_name) if db_name in pool_dic: pool = pool_dic[db_name] @@ -66,18 +68,6 @@ def restart_pool(db_name, force_demo=False, status=None, update_module=False): return get_db_and_pool(db_name, force_demo, status, update_module=update_module) -def get_db_only(db_name): - """Return a database connection.""" - # ATTENTION: - # do not put this import outside this function - # sql_db must not be loaded before the logger is initialized. - # sql_db import psycopg2.tool which create a default logger if there is not. - # this resulting of having the logs outputed twice... - import openerp.sql_db as sql_db - db = sql_db.db_connect(db_name) - return db - - def get_db(db_name): """Return a database connection. The corresponding osv_pool is initialize.""" return get_db_and_pool(db_name)[0] diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index 2455d250a94..95194679198 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -154,6 +154,7 @@ class db(netsvc.ExportService): def exp_drop(self, db_name): sql_db.close_db(db_name) + openerp.netsvc.Agent.cancel(db_name) logger = netsvc.Logger() db = sql_db.db_connect('template1') @@ -256,6 +257,7 @@ class db(netsvc.ExportService): def exp_rename(self, old_name, new_name): sql_db.close_db(old_name) + openerp.netsvc.Agent.cancel(db_name) logger = netsvc.Logger() db = sql_db.db_connect('template1') diff --git a/openerp/sql_db.py b/openerp/sql_db.py index 8afd684d3f1..4b1d8aab794 100644 --- a/openerp/sql_db.py +++ b/openerp/sql_db.py @@ -54,7 +54,6 @@ psycopg2.extensions.register_type(psycopg2.extensions.new_type((700, 701, 1700,) import tools from tools.func import wraps, frame_codeinfo -from netsvc import Agent from datetime import datetime as mdt from datetime import timedelta import threading @@ -382,8 +381,8 @@ def db_connect(db_name): return Connection(_Pool, db_name) def close_db(db_name): + """ You might want to call openerp.netsvc.Agent.cancel(db_name) along this function.""" _Pool.close_all(dsn(db_name)) - Agent.cancel(db_name) tools.cache.clean_caches_for_db(db_name) ct = currentThread() if hasattr(ct, 'dbname'): diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 9723135ff49..5761ba87cc0 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -27,6 +27,7 @@ import itertools import locale import os import openerp.pooler as pooler +import openerp.sql_db as sql_db import re import logging import tarfile @@ -160,7 +161,7 @@ class GettextAlias(object): # find current DB based on thread/worker db name (see netsvc) db_name = getattr(threading.currentThread(), 'dbname', None) if db_name: - return pooler.get_db_only(db_name) + return sql_db.db_connect(db_name) def _get_cr(self, frame): is_new_cr = False From 545a438c039acc114507072efdc686eea410ae4d Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 20 Apr 2011 17:27:18 +0200 Subject: [PATCH 07/35] [REF] openerp.modules - for the moment it is a copy of openerp.addons bzr revid: vmt@openerp.com-20110420152718-eok61oule6p2x58z --- openerp/__init__.py | 1 + openerp/addons/__init__.py | 967 +------------------------ openerp/addons/base/ir/ir_ui_menu.py | 4 +- openerp/addons/base/module/module.py | 2 +- openerp/db/__init__.py | 30 + openerp/modules/__init__.py | 1002 ++++++++++++++++++++++++++ openerp/pooler.py | 4 +- openerp/report/interface.py | 4 +- openerp/service/web_services.py | 10 +- openerp/tools/misc.py | 4 +- 10 files changed, 1056 insertions(+), 972 deletions(-) create mode 100644 openerp/db/__init__.py create mode 100644 openerp/modules/__init__.py diff --git a/openerp/__init__.py b/openerp/__init__.py index c5f7732edd0..02d16401293 100644 --- a/openerp/__init__.py +++ b/openerp/__init__.py @@ -22,6 +22,7 @@ import addons import ir import loglevels +import modules import netsvc import osv import pooler diff --git a/openerp/addons/__init__.py b/openerp/addons/__init__.py index c2c6001e04a..d019985f34c 100644 --- a/openerp/addons/__init__.py +++ b/openerp/addons/__init__.py @@ -20,966 +20,17 @@ # ############################################################################## -import os, sys, imp -from os.path import join as opj -import itertools -import zipimport +""" Addons module. -import openerp.osv as osv -import openerp.tools as tools -import openerp.tools.osutil as osutil -from openerp.tools.safe_eval import safe_eval as eval -import openerp.pooler as pooler -from openerp.tools.translate import _ +This module only serves to contain OpenERP addons. For the code to +manage those addons, see openerp.modules. This module conveniently +reexports some symbols from openerp.modules. Importing them from here +is deprecated. -import openerp.netsvc as netsvc - -import zipfile -import openerp.release as release - -import re -import base64 -from zipfile import PyZipFile, ZIP_DEFLATED -from cStringIO import StringIO - -import logging - -logger = netsvc.Logger() - -_ad = os.path.dirname(__file__) # default addons path (base) -ad_paths = [] - -# Modules already loaded -loaded = [] - -def initialize_sys_path(): - global ad_paths - - if ad_paths: - return - - ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(',')) - - sys.path.insert(1, _ad) - - ad_cnt=1 - for adp in ad_paths: - if adp != _ad: - sys.path.insert(ad_cnt, adp) - ad_cnt+=1 - - ad_paths.append(_ad) # for get_module_path - -class Graph(dict): - - def addNode(self, name, deps): - max_depth, father = 0, None - for n in [Node(x, self) for x in deps]: - if n.depth >= max_depth: - father = n - max_depth = n.depth - if father: - father.addChild(name) - else: - Node(name, self) - - def update_from_db(self, cr): - if not len(self): - return - # update the graph with values from the database (if exist) - ## First, we set the default values for each package in graph - additional_data = dict.fromkeys(self.keys(), {'id': 0, 'state': 'uninstalled', 'dbdemo': False, 'installed_version': None}) - ## Then we get the values from the database - cr.execute('SELECT name, id, state, demo AS dbdemo, latest_version AS installed_version' - ' FROM ir_module_module' - ' WHERE name IN %s',(tuple(additional_data),) - ) - - ## and we update the default values with values from the database - additional_data.update(dict([(x.pop('name'), x) for x in cr.dictfetchall()])) - - for package in self.values(): - for k, v in additional_data[package.name].items(): - setattr(package, k, v) - - def __iter__(self): - level = 0 - done = set(self.keys()) - while done: - level_modules = [(name, module) for name, module in self.items() if module.depth==level] - for name, module in level_modules: - done.remove(name) - yield module - level += 1 - -class Singleton(object): - def __new__(cls, name, graph): - if name in graph: - inst = graph[name] - else: - inst = object.__new__(cls) - inst.name = name - graph[name] = inst - return inst - - -class Node(Singleton): - - def __init__(self, name, graph): - self.graph = graph - if not hasattr(self, 'children'): - self.children = [] - if not hasattr(self, 'depth'): - self.depth = 0 - - def addChild(self, name): - node = Node(name, self.graph) - node.depth = self.depth + 1 - if node not in self.children: - self.children.append(node) - for attr in ('init', 'update', 'demo'): - if hasattr(self, attr): - setattr(node, attr, True) - self.children.sort(lambda x, y: cmp(x.name, y.name)) - - def __setattr__(self, name, value): - super(Singleton, self).__setattr__(name, value) - if name in ('init', 'update', 'demo'): - tools.config[name][self.name] = 1 - for child in self.children: - setattr(child, name, value) - if name == 'depth': - for child in self.children: - setattr(child, name, value + 1) - - def __iter__(self): - return itertools.chain(iter(self.children), *map(iter, self.children)) - - def __str__(self): - return self._pprint() - - def _pprint(self, depth=0): - s = '%s\n' % self.name - for c in self.children: - s += '%s`-> %s' % (' ' * depth, c._pprint(depth+1)) - return s - - -def get_module_path(module, downloaded=False): - """Return the path of the given module. - - Search the addons paths and return the first path where the given - module is found. If downloaded is True, return the default addons - path if nothing else is found. - - """ - initialize_sys_path() - for adp in ad_paths: - if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)): - return opj(adp, module) - - if downloaded: - return opj(_ad, module) - logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,)) - return False - - -def get_module_filetree(module, dir='.'): - path = get_module_path(module) - if not path: - return False - - dir = os.path.normpath(dir) - if dir == '.': - dir = '' - if dir.startswith('..') or (dir and dir[0] == '/'): - raise Exception('Cannot access file outside the module') - - if not os.path.isdir(path): - # zipmodule - zip = zipfile.ZipFile(path + ".zip") - files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()] - else: - files = osutil.listdir(path, True) - - tree = {} - for f in files: - if not f.startswith(dir): - continue - - if dir: - f = f[len(dir)+int(not dir.endswith('/')):] - lst = f.split(os.sep) - current = tree - while len(lst) != 1: - current = current.setdefault(lst.pop(0), {}) - current[lst.pop(0)] = None - - return tree - -def zip_directory(directory, b64enc=True, src=True): - """Compress a directory - - @param directory: The directory to compress - @param base64enc: if True the function will encode the zip file with base64 - @param src: Integrate the source files - - @return: a string containing the zip file - """ - - RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I) - - def _zippy(archive, path, src=True): - path = os.path.abspath(path) - base = os.path.basename(path) - for f in osutil.listdir(path, True): - bf = os.path.basename(f) - if not RE_exclude.search(bf) and (src or bf in ('__openerp__.py', '__terp__.py') or not bf.endswith('.py')): - archive.write(os.path.join(path, f), os.path.join(base, f)) - - archname = StringIO() - archive = PyZipFile(archname, "w", ZIP_DEFLATED) - - # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8) - directory = tools.ustr(directory).encode('utf-8') - - archive.writepy(directory) - _zippy(archive, directory, src=src) - archive.close() - archive_data = archname.getvalue() - archname.close() - - if b64enc: - return base64.encodestring(archive_data) - - return archive_data - -def get_module_as_zip(modulename, b64enc=True, src=True): - """Generate a module as zip file with the source or not and can do a base64 encoding - - @param modulename: The module name - @param b64enc: if True the function will encode the zip file with base64 - @param src: Integrate the source files - - @return: a stream to store in a file-like object - """ - - ap = get_module_path(str(modulename)) - if not ap: - raise Exception('Unable to find path for module %s' % modulename) - - ap = ap.encode('utf8') - if os.path.isfile(ap + '.zip'): - val = file(ap + '.zip', 'rb').read() - if b64enc: - val = base64.encodestring(val) - else: - val = zip_directory(ap, b64enc, src) - - return val - - -def get_module_resource(module, *args): - """Return the full path of a resource of the given module. - - @param module: the module - @param args: the resource path components - - @return: absolute path to the resource - """ - a = get_module_path(module) - if not a: return False - resource_path = opj(a, *args) - if zipfile.is_zipfile( a +'.zip') : - zip = zipfile.ZipFile( a + ".zip") - files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()] - resource_path = '/'.join(args) - if resource_path in files: - return opj(a, resource_path) - elif os.path.exists(resource_path): - return resource_path - return False - -def get_modules(): - """Returns the list of module names - """ - def listdir(dir): - def clean(name): - name = os.path.basename(name) - if name[-4:] == '.zip': - name = name[:-4] - return name - - def is_really_module(name): - name = opj(dir, name) - return os.path.isdir(name) or zipfile.is_zipfile(name) - return map(clean, filter(is_really_module, os.listdir(dir))) - - plist = [] - initialize_sys_path() - for ad in ad_paths: - plist.extend(listdir(ad)) - return list(set(plist)) - -def load_information_from_description_file(module): - """ - :param module: The name of the module (sale, purchase, ...) - """ - - for filename in ['__openerp__.py', '__terp__.py']: - description_file = get_module_resource(module, filename) - if description_file : - desc_f = tools.file_open(description_file) - try: - return eval(desc_f.read()) - finally: - desc_f.close() - - #TODO: refactor the logger in this file to follow the logging guidelines - # for 6.0 - logging.getLogger('addons').debug('The module %s does not contain a description file:'\ - '__openerp__.py or __terp__.py (deprecated)', module) - return {} - -def get_modules_with_version(): - modules = get_modules() - res = {} - for module in modules: - try: - info = load_information_from_description_file(module) - res[module] = "%s.%s" % (release.major_version, info['version']) - except Exception, e: - continue - return res - -def create_graph(cr, module_list, force=None): - graph = Graph() - upgrade_graph(graph, cr, module_list, force) - return graph - -def upgrade_graph(graph, cr, module_list, force=None): - if force is None: - force = [] - packages = [] - len_graph = len(graph) - for module in module_list: - mod_path = get_module_path(module) - terp_file = get_module_resource(module, '__openerp__.py') - if not terp_file: - terp_file = get_module_resource(module, '__terp__.py') - if not mod_path or not terp_file: - logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not found, skipped' % (module)) - continue - - if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'): - terp_f = tools.file_open(terp_file) - try: - info = eval(terp_f.read()) - except Exception: - logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: eval file %s' % (module, terp_file)) - raise - finally: - terp_f.close() - if info.get('installable', True): - packages.append((module, info.get('depends', []), info)) - else: - logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not installable, skipped' % (module)) - - dependencies = dict([(p, deps) for p, deps, data in packages]) - current, later = set([p for p, dep, data in packages]), set() - - while packages and current > later: - package, deps, data = packages[0] - - # if all dependencies of 'package' are already in the graph, add 'package' in the graph - if reduce(lambda x, y: x and y in graph, deps, True): - if not package in current: - packages.pop(0) - continue - later.clear() - current.remove(package) - graph.addNode(package, deps) - node = Node(package, graph) - node.data = data - for kind in ('init', 'demo', 'update'): - if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force: - setattr(node, kind, True) - else: - later.add(package) - packages.append((package, deps, data)) - packages.pop(0) - - graph.update_from_db(cr) - - for package in later: - unmet_deps = filter(lambda p: p not in graph, dependencies[package]) - logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: Unmet dependencies: %s' % (package, ', '.join(unmet_deps))) - - result = len(graph) - len_graph - if result != len(module_list): - logger.notifyChannel('init', netsvc.LOG_WARNING, 'Not all modules have loaded.') - return result - - -def init_module_objects(cr, module_name, obj_list): - logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: creating or updating database tables' % module_name) - todo = [] - for obj in obj_list: - try: - result = obj._auto_init(cr, {'module': module_name}) - except Exception, e: - raise - if result: - todo += result - if hasattr(obj, 'init'): - obj.init(cr) - cr.commit() - todo.sort() - for t in todo: - t[1](cr, *t[2]) - cr.commit() - - -def register_class(m): - """ - Register module named m, if not already registered - """ - - def log(e): - mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or '' - msg = "Couldn't load %smodule %s" % (mt, m) - logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg) - logger.notifyChannel('init', netsvc.LOG_CRITICAL, e) - - global loaded - if m in loaded: - return - logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m) - mod_path = get_module_path(m) - - initialize_sys_path() - try: - zip_mod_path = mod_path + '.zip' - if not os.path.isfile(zip_mod_path): - fm = imp.find_module(m, ad_paths) - try: - imp.load_module(m, *fm) - finally: - if fm[0]: - fm[0].close() - else: - zimp = zipimport.zipimporter(zip_mod_path) - zimp.load_module(m) - except Exception, e: - log(e) - raise - else: - loaded.append(m) - - -class MigrationManager(object): - """ - This class manage the migration of modules - Migrations files must be python files containing a "migrate(cr, installed_version)" function. - Theses files must respect a directory tree structure: A 'migrations' folder which containt a - folder by version. Version can be 'module' version or 'server.module' version (in this case, - the files will only be processed by this version of the server). Python file names must start - by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation - Example: - - - `-- migrations - |-- 1.0 - | |-- pre-update_table_x.py - | |-- pre-update_table_y.py - | |-- post-clean-data.py - | `-- README.txt # not processed - |-- 5.0.1.1 # files in this folder will be executed only on a 5.0 server - | |-- pre-delete_table_z.py - | `-- post-clean-data.py - `-- foo.py # not processed - - This similar structure is generated by the maintenance module with the migrations files get by - the maintenance contract - - """ - def __init__(self, cr, graph): - self.cr = cr - self.graph = graph - self.migrations = {} - self._get_files() - - def _get_files(self): - - """ - import addons.base.maintenance.utils as maintenance_utils - maintenance_utils.update_migrations_files(self.cr) - #""" - - for pkg in self.graph: - self.migrations[pkg.name] = {} - if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'): - continue - - self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {} - self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {} - - def migrate_module(self, pkg, stage): - assert stage in ('pre', 'post') - stageformat = {'pre': '[>%s]', - 'post': '[%s>]', - } - - if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'): - return - - def convert_version(version): - if version.startswith(release.major_version) and version != release.major_version: - return version # the version number already containt the server version - return "%s.%s" % (release.major_version, version) - - def _get_migration_versions(pkg): - def __get_dir(tree): - return [d for d in tree if tree[d] is not None] - - versions = list(set( - __get_dir(self.migrations[pkg.name]['module']) + - __get_dir(self.migrations[pkg.name]['maintenance']) - )) - versions.sort(key=lambda k: parse_version(convert_version(k))) - return versions - - def _get_migration_files(pkg, version, stage): - """ return a list of tuple (module, file) - """ - m = self.migrations[pkg.name] - lst = [] - - mapping = {'module': opj(pkg.name, 'migrations'), - 'maintenance': opj('base', 'maintenance', 'migrations', pkg.name), - } - - for x in mapping.keys(): - if version in m[x]: - for f in m[x][version]: - if m[x][version][f] is not None: - continue - if not f.startswith(stage + '-'): - continue - lst.append(opj(mapping[x], version, f)) - lst.sort() - return lst - - def mergedict(a, b): - a = a.copy() - a.update(b) - return a - - from openerp.tools.parse_version import parse_version - - parsed_installed_version = parse_version(pkg.installed_version or '') - current_version = parse_version(convert_version(pkg.data.get('version', '0'))) - - versions = _get_migration_versions(pkg) - - for version in versions: - if parsed_installed_version < parse_version(convert_version(version)) <= current_version: - - strfmt = {'addon': pkg.name, - 'stage': stage, - 'version': stageformat[stage] % version, - } - - for pyfile in _get_migration_files(pkg, version, stage): - name, ext = os.path.splitext(os.path.basename(pyfile)) - if ext.lower() != '.py': - continue - mod = fp = fp2 = None - try: - fp = tools.file_open(pyfile) - - # imp.load_source need a real file object, so we create - # one from the file-like object we get from file_open - fp2 = os.tmpfile() - fp2.write(fp.read()) - fp2.seek(0) - try: - mod = imp.load_source(name, pyfile, fp2) - logger.notifyChannel('migration', netsvc.LOG_INFO, 'module %(addon)s: Running migration %(version)s %(name)s' % mergedict({'name': mod.__name__}, strfmt)) - mod.migrate(self.cr, pkg.installed_version) - except ImportError: - logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % mergedict({'file': pyfile}, strfmt)) - raise - except AttributeError: - logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt) - except: - raise - finally: - if fp: - fp.close() - if fp2: - fp2.close() - if mod: - del mod - -log = logging.getLogger('init') - -def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, **kwargs): - """Migrates+Updates or Installs all module nodes from ``graph`` - :param graph: graph of module nodes to load - :param status: status dictionary for keeping track of progress - :param perform_checks: whether module descriptors should be checked for validity (prints warnings - for same cases, and even raise osv_except if certificate is invalid) - :param skip_modules: optional list of module names (packages) which have previously been loaded and can be skipped - :return: list of modules that were installed or updated - """ - def process_sql_file(cr, fp): - queries = fp.read().split(';') - for query in queries: - new_query = ' '.join(query.split()) - if new_query: - cr.execute(new_query) - - def load_init_update_xml(cr, m, idref, mode, kind): - for filename in package.data.get('%s_xml' % kind, []): - logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, filename)) - _, ext = os.path.splitext(filename) - fp = tools.file_open(opj(m, filename)) - try: - if ext == '.csv': - noupdate = (kind == 'init') - tools.convert_csv_import(cr, m, os.path.basename(filename), fp.read(), idref, mode=mode, noupdate=noupdate) - elif ext == '.sql': - process_sql_file(cr, fp) - elif ext == '.yml': - tools.convert_yaml_import(cr, m, fp, idref, mode=mode, **kwargs) - else: - tools.convert_xml_import(cr, m, fp, idref, mode=mode, **kwargs) - finally: - fp.close() - - def load_demo_xml(cr, m, idref, mode): - for xml in package.data.get('demo_xml', []): - name, ext = os.path.splitext(xml) - logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, xml)) - fp = tools.file_open(opj(m, xml)) - try: - if ext == '.csv': - tools.convert_csv_import(cr, m, os.path.basename(xml), fp.read(), idref, mode=mode, noupdate=True) - elif ext == '.yml': - tools.convert_yaml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs) - else: - tools.convert_xml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs) - finally: - fp.close() - - def load_data(cr, module_name, id_map, mode): - _load_data(cr, module_name, id_map, mode, 'data') - - def load_demo(cr, module_name, id_map, mode): - _load_data(cr, module_name, id_map, mode, 'demo') - - def load_test(cr, module_name, id_map, mode): - cr.commit() - if not tools.config.options['test_disable']: - try: - _load_data(cr, module_name, id_map, mode, 'test') - except Exception, e: - logging.getLogger('test').exception('Tests failed to execute in module %s', module_name) - finally: - if tools.config.options['test_commit']: - cr.commit() - else: - cr.rollback() - - def _load_data(cr, module_name, id_map, mode, kind): - for filename in package.data.get(kind, []): - noupdate = (kind == 'demo') - _, ext = os.path.splitext(filename) - log.info("module %s: loading %s", module_name, filename) - pathname = os.path.join(module_name, filename) - file = tools.file_open(pathname) - try: - if ext == '.sql': - process_sql_file(cr, file) - elif ext == '.csv': - noupdate = (kind == 'init') - tools.convert_csv_import(cr, module_name, pathname, file.read(), id_map, mode, noupdate) - elif ext == '.yml': - tools.convert_yaml_import(cr, module_name, file, id_map, mode, noupdate) - else: - tools.convert_xml_import(cr, module_name, file, id_map, mode, noupdate) - finally: - file.close() - - # **kwargs is passed directly to convert_xml_import - if not status: - status = {} - - status = status.copy() - processed_modules = [] - statusi = 0 - pool = pooler.get_pool(cr.dbname) - migrations = MigrationManager(cr, graph) - modobj = None - logger.notifyChannel('init', netsvc.LOG_DEBUG, 'loading %d packages..' % len(graph)) - - for package in graph: - if skip_modules and package.name in skip_modules: - continue - logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name) - migrations.migrate_module(package, 'pre') - register_class(package.name) - modules = pool.instanciate(package.name, cr) - if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'): - init_module_objects(cr, package.name, modules) - cr.commit() - - for package in graph: - status['progress'] = (float(statusi)+0.1) / len(graph) - m = package.name - mid = package.id - - if skip_modules and m in skip_modules: - continue - - if modobj is None: - modobj = pool.get('ir.module.module') - - if modobj and perform_checks: - modobj.check(cr, 1, [mid]) - - idref = {} - status['progress'] = (float(statusi)+0.4) / len(graph) - - mode = 'update' - if hasattr(package, 'init') or package.state == 'to install': - mode = 'init' - - if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'): - for kind in ('init', 'update'): - if package.state=='to upgrade': - # upgrading the module information - modobj.write(cr, 1, [mid], modobj.get_values_from_terp(package.data)) - load_init_update_xml(cr, m, idref, mode, kind) - load_data(cr, m, idref, mode) - if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'): - status['progress'] = (float(statusi)+0.75) / len(graph) - load_demo_xml(cr, m, idref, mode) - load_demo(cr, m, idref, mode) - cr.execute('update ir_module_module set demo=%s where id=%s', (True, mid)) - - # launch tests only in demo mode, as most tests will depend - # on demo data. Other tests can be added into the regular - # 'data' section, but should probably not alter the data, - # as there is no rollback. - load_test(cr, m, idref, mode) - - processed_modules.append(package.name) - - migrations.migrate_module(package, 'post') - - if modobj: - ver = release.major_version + '.' + package.data.get('version', '1.0') - # Set new modules and dependencies - modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver}) - cr.commit() - # Update translations for all installed languages - modobj.update_translations(cr, 1, [mid], None, {'overwrite': tools.config['overwrite_existing_translations']}) - cr.commit() - - package.state = 'installed' - for kind in ('init', 'demo', 'update'): - if hasattr(package, kind): - delattr(package, kind) - - statusi += 1 - - cr.commit() - - return processed_modules - -def _check_module_names(cr, module_names): - mod_names = set(module_names) - if 'base' in mod_names: - # ignore dummy 'all' module - if 'all' in mod_names: - mod_names.remove('all') - if mod_names: - cr.execute("SELECT count(id) AS count FROM ir_module_module WHERE name in %s", (tuple(mod_names),)) - if cr.dictfetchone()['count'] != len(mod_names): - # find out what module name(s) are incorrect: - cr.execute("SELECT name FROM ir_module_module") - incorrect_names = mod_names.difference([x['name'] for x in cr.dictfetchall()]) - logging.getLogger('init').warning('invalid module names, ignored: %s', ", ".join(incorrect_names)) - -def load_modules(db, force_demo=False, status=None, update_module=False): - - initialize_sys_path() - - # Backward compatibility: addons don't have to import openerp.xxx, they still can import xxx - for k, v in list(sys.modules.items()): - if k.startswith('openerp.') and sys.modules.get(k[8:]) is None: - sys.modules[k[8:]] = v - - if not status: - status = {} - cr = db.cursor() - if cr: - cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='ir_module_module'") - if len(cr.fetchall())==0: - logger.notifyChannel("init", netsvc.LOG_INFO, "init db") - tools.init_db(cr) - tools.config["init"]["all"] = 1 - tools.config['update']['all'] = 1 - if not tools.config['without_demo']: - tools.config["demo"]['all'] = 1 - force = [] - if force_demo: - force.append('demo') - - # This is a brand new pool, just created in pooler.get_db_and_pool() - pool = pooler.get_pool(cr.dbname) - - try: - processed_modules = [] - report = tools.assertion_report() - # NOTE: Try to also load the modules that have been marked as uninstallable previously... - STATES_TO_LOAD = ['installed', 'to upgrade', 'uninstallable'] - if 'base' in tools.config['update'] or 'all' in tools.config['update']: - cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed')) - - # STEP 1: LOAD BASE (must be done before module dependencies can be computed for later steps) - graph = create_graph(cr, ['base'], force) - if not graph: - logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)') - raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)')) - processed_modules.extend(load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report)) - - if tools.config['load_language']: - for lang in tools.config['load_language'].split(','): - tools.load_language(cr, lang) - - # STEP 2: Mark other modules to be loaded/updated - if update_module: - modobj = pool.get('ir.module.module') - logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list') - if ('base' in tools.config['init']) or ('base' in tools.config['update']): - modobj.update_list(cr, 1) - - _check_module_names(cr, itertools.chain(tools.config['init'].keys(), tools.config['update'].keys())) - - mods = [k for k in tools.config['init'] if tools.config['init'][k]] - if mods: - ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)]) - if ids: - modobj.button_install(cr, 1, ids) - - mods = [k for k in tools.config['update'] if tools.config['update'][k]] - if mods: - ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)]) - if ids: - modobj.button_upgrade(cr, 1, ids) - - cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base')) - - STATES_TO_LOAD += ['to install'] - - - # STEP 3: Load marked modules (skipping base which was done in STEP 1) - loop_guardrail = 0 - while True: - loop_guardrail += 1 - if loop_guardrail > 100: - raise ValueError('Possible recursive module tree detected, aborting.') - cr.execute("SELECT name from ir_module_module WHERE state IN %s" ,(tuple(STATES_TO_LOAD),)) - - module_list = [name for (name,) in cr.fetchall() if name not in graph] - if not module_list: - break - - new_modules_in_graph = upgrade_graph(graph, cr, module_list, force) - if new_modules_in_graph == 0: - # nothing to load - break - - logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list))) - processed_modules.extend(load_module_graph(cr, graph, status, report=report, skip_modules=processed_modules)) - - # load custom models - cr.execute('select model from ir_model where state=%s', ('manual',)) - for model in cr.dictfetchall(): - pool.get('ir.model').instanciate(cr, 1, model['model'], {}) - - # STEP 4: Finish and cleanup - if processed_modules: - cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""") - for (model, name) in cr.fetchall(): - model_obj = pool.get(model) - if model_obj and not isinstance(model_obj, osv.osv.osv_memory): - logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name)) - - # Temporary warning while we remove access rights on osv_memory objects, as they have - # been replaced by owner-only access rights - cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""") - for (model, name) in cr.fetchall(): - model_obj = pool.get(model) - if isinstance(model_obj, osv.osv.osv_memory): - logger.notifyChannel('init', netsvc.LOG_WARNING, 'In-memory object %s (%s) should not have explicit access rules!' % (model, name)) - - cr.execute("SELECT model from ir_model") - for (model,) in cr.fetchall(): - obj = pool.get(model) - if obj: - obj._check_removed_columns(cr, log=True) - else: - logger.notifyChannel('init', netsvc.LOG_WARNING, "Model %s is referenced but not present in the orm pool!" % model) - - # Cleanup orphan records - pool.get('ir.model.data')._process_end(cr, 1, processed_modules) - - if report.get_report(): - logger.notifyChannel('init', netsvc.LOG_INFO, report) - - for kind in ('init', 'demo', 'update'): - tools.config[kind] = {} - - cr.commit() - if update_module: - cr.execute("select id,name from ir_module_module where state=%s", ('to remove',)) - for mod_id, mod_name in cr.fetchall(): - cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,)) - for rmod, rid in cr.fetchall(): - uid = 1 - rmod_module= pool.get(rmod) - if rmod_module: - rmod_module.unlink(cr, uid, [rid]) - else: - logger.notifyChannel('init', netsvc.LOG_ERROR, 'Could not locate %s to remove res=%d' % (rmod,rid)) - cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,)) - cr.commit() - # - # TODO: remove menu without actions of children - # - while True: - cr.execute('''delete from - ir_ui_menu - where - (id not IN (select parent_id from ir_ui_menu where parent_id is not null)) - and - (id not IN (select res_id from ir_values where model='ir.ui.menu')) - and - (id not IN (select res_id from ir_model_data where model='ir.ui.menu'))''') - cr.commit() - if not cr.rowcount: - break - else: - logger.notifyChannel('init', netsvc.LOG_INFO, 'removed %d unused menus' % (cr.rowcount,)) - - cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',)) - cr.commit() - finally: - cr.close() +""" +from openerp.modules import + get_module_resource, + get_module_path # used only by base_module_quality # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index 99a13b46cff..d67f0c247d1 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -24,7 +24,7 @@ import base64 import re import tools -import addons +import openerp.modules from osv import fields, osv from tools.translate import _ @@ -229,7 +229,7 @@ class ir_ui_menu(osv.osv): if not path: return False path_info = path.split(',') - icon_path = addons.get_module_resource(path_info[0],path_info[1]) + icon_path = openerp.modules.get_module_resource(path_info[0],path_info[1]) icon_image = False if icon_path: try: diff --git a/openerp/addons/base/module/module.py b/openerp/addons/base/module/module.py index 5458771b49d..36a5f80dfa3 100644 --- a/openerp/addons/base/module/module.py +++ b/openerp/addons/base/module/module.py @@ -30,7 +30,7 @@ import urllib import zipfile import zipimport -import addons +import openerp.modules as addons import pooler import release import tools diff --git a/openerp/db/__init__.py b/openerp/db/__init__.py new file mode 100644 index 00000000000..5eb06af1ad1 --- /dev/null +++ b/openerp/db/__init__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2011 OpenERP s.a. (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +""" Lower-level database access. + +This module provides access to the underlying database without going +through the ORM. The goal is to gather sql_db.py and other various db +code. + +""" + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py new file mode 100644 index 00000000000..2c5c3e037fc --- /dev/null +++ b/openerp/modules/__init__.py @@ -0,0 +1,1002 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# Copyright (C) 2010-2011 OpenERP s.a. (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +""" Modules (also called addons) management. + +""" + +import os, sys, imp +from os.path import join as opj +import itertools +import zipimport + +import openerp.osv as osv +import openerp.tools as tools +import openerp.tools.osutil as osutil +from openerp.tools.safe_eval import safe_eval as eval +import openerp.pooler as pooler +from openerp.tools.translate import _ + +import openerp.netsvc as netsvc + +import zipfile +import openerp.release as release + +import re +import base64 +from zipfile import PyZipFile, ZIP_DEFLATED +from cStringIO import StringIO + +import logging + +logger = netsvc.Logger() + +_ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base) +ad_paths = [] + +# Modules already loaded +loaded = [] + +def initialize_sys_path(): + global ad_paths + + if ad_paths: + return + + ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(',')) + + sys.path.insert(1, _ad) + + ad_cnt=1 + for adp in ad_paths: + if adp != _ad: + sys.path.insert(ad_cnt, adp) + ad_cnt+=1 + + ad_paths.append(_ad) # for get_module_path + +class Graph(dict): + """ Modules dependency graph. + + The graph is a mapping from module name to Nodes. + + """ + + def addNode(self, name, deps): + max_depth, father = 0, None + for n in [Node(x, self) for x in deps]: + if n.depth >= max_depth: + father = n + max_depth = n.depth + if father: + father.addChild(name) + else: + Node(name, self) + + def update_from_db(self, cr): + if not len(self): + return + # update the graph with values from the database (if exist) + ## First, we set the default values for each package in graph + additional_data = dict.fromkeys(self.keys(), {'id': 0, 'state': 'uninstalled', 'dbdemo': False, 'installed_version': None}) + ## Then we get the values from the database + cr.execute('SELECT name, id, state, demo AS dbdemo, latest_version AS installed_version' + ' FROM ir_module_module' + ' WHERE name IN %s',(tuple(additional_data),) + ) + + ## and we update the default values with values from the database + additional_data.update(dict([(x.pop('name'), x) for x in cr.dictfetchall()])) + + for package in self.values(): + for k, v in additional_data[package.name].items(): + setattr(package, k, v) + + def __iter__(self): + level = 0 + done = set(self.keys()) + while done: + level_modules = [(name, module) for name, module in self.items() if module.depth==level] + for name, module in level_modules: + done.remove(name) + yield module + level += 1 + +class Singleton(object): + def __new__(cls, name, graph): + if name in graph: + inst = graph[name] + else: + inst = object.__new__(cls) + inst.name = name + graph[name] = inst + return inst + + +class Node(Singleton): + """ One module in the modules dependency graph. + + Node acts as a per-module singleton. + + """ + + def __init__(self, name, graph): + self.graph = graph + if not hasattr(self, 'children'): + self.children = [] + if not hasattr(self, 'depth'): + self.depth = 0 + + def addChild(self, name): + node = Node(name, self.graph) + node.depth = self.depth + 1 + if node not in self.children: + self.children.append(node) + for attr in ('init', 'update', 'demo'): + if hasattr(self, attr): + setattr(node, attr, True) + self.children.sort(lambda x, y: cmp(x.name, y.name)) + + def __setattr__(self, name, value): + super(Singleton, self).__setattr__(name, value) + if name in ('init', 'update', 'demo'): + tools.config[name][self.name] = 1 + for child in self.children: + setattr(child, name, value) + if name == 'depth': + for child in self.children: + setattr(child, name, value + 1) + + def __iter__(self): + return itertools.chain(iter(self.children), *map(iter, self.children)) + + def __str__(self): + return self._pprint() + + def _pprint(self, depth=0): + s = '%s\n' % self.name + for c in self.children: + s += '%s`-> %s' % (' ' * depth, c._pprint(depth+1)) + return s + + +def get_module_path(module, downloaded=False): + """Return the path of the given module. + + Search the addons paths and return the first path where the given + module is found. If downloaded is True, return the default addons + path if nothing else is found. + + """ + initialize_sys_path() + for adp in ad_paths: + if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)): + return opj(adp, module) + + if downloaded: + return opj(_ad, module) + logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,)) + return False + + +def get_module_filetree(module, dir='.'): + path = get_module_path(module) + if not path: + return False + + dir = os.path.normpath(dir) + if dir == '.': + dir = '' + if dir.startswith('..') or (dir and dir[0] == '/'): + raise Exception('Cannot access file outside the module') + + if not os.path.isdir(path): + # zipmodule + zip = zipfile.ZipFile(path + ".zip") + files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()] + else: + files = osutil.listdir(path, True) + + tree = {} + for f in files: + if not f.startswith(dir): + continue + + if dir: + f = f[len(dir)+int(not dir.endswith('/')):] + lst = f.split(os.sep) + current = tree + while len(lst) != 1: + current = current.setdefault(lst.pop(0), {}) + current[lst.pop(0)] = None + + return tree + +def zip_directory(directory, b64enc=True, src=True): + """Compress a directory + + @param directory: The directory to compress + @param base64enc: if True the function will encode the zip file with base64 + @param src: Integrate the source files + + @return: a string containing the zip file + """ + + RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I) + + def _zippy(archive, path, src=True): + path = os.path.abspath(path) + base = os.path.basename(path) + for f in osutil.listdir(path, True): + bf = os.path.basename(f) + if not RE_exclude.search(bf) and (src or bf in ('__openerp__.py', '__terp__.py') or not bf.endswith('.py')): + archive.write(os.path.join(path, f), os.path.join(base, f)) + + archname = StringIO() + archive = PyZipFile(archname, "w", ZIP_DEFLATED) + + # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8) + directory = tools.ustr(directory).encode('utf-8') + + archive.writepy(directory) + _zippy(archive, directory, src=src) + archive.close() + archive_data = archname.getvalue() + archname.close() + + if b64enc: + return base64.encodestring(archive_data) + + return archive_data + +def get_module_as_zip(modulename, b64enc=True, src=True): + """Generate a module as zip file with the source or not and can do a base64 encoding + + @param modulename: The module name + @param b64enc: if True the function will encode the zip file with base64 + @param src: Integrate the source files + + @return: a stream to store in a file-like object + """ + + ap = get_module_path(str(modulename)) + if not ap: + raise Exception('Unable to find path for module %s' % modulename) + + ap = ap.encode('utf8') + if os.path.isfile(ap + '.zip'): + val = file(ap + '.zip', 'rb').read() + if b64enc: + val = base64.encodestring(val) + else: + val = zip_directory(ap, b64enc, src) + + return val + + +def get_module_resource(module, *args): + """Return the full path of a resource of the given module. + + @param module: the module + @param args: the resource path components + + @return: absolute path to the resource + + TODO name it get_resource_path + TODO make it available inside on osv object (self.get_resource_path) + """ + a = get_module_path(module) + if not a: return False + resource_path = opj(a, *args) + if zipfile.is_zipfile( a +'.zip') : + zip = zipfile.ZipFile( a + ".zip") + files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()] + resource_path = '/'.join(args) + if resource_path in files: + return opj(a, resource_path) + elif os.path.exists(resource_path): + return resource_path + return False + +def get_modules(): + """Returns the list of module names + """ + def listdir(dir): + def clean(name): + name = os.path.basename(name) + if name[-4:] == '.zip': + name = name[:-4] + return name + + def is_really_module(name): + name = opj(dir, name) + return os.path.isdir(name) or zipfile.is_zipfile(name) + return map(clean, filter(is_really_module, os.listdir(dir))) + + plist = [] + initialize_sys_path() + for ad in ad_paths: + plist.extend(listdir(ad)) + return list(set(plist)) + +def load_information_from_description_file(module): + """ + :param module: The name of the module (sale, purchase, ...) + """ + + for filename in ['__openerp__.py', '__terp__.py']: + description_file = get_module_resource(module, filename) + if description_file : + desc_f = tools.file_open(description_file) + try: + return eval(desc_f.read()) + finally: + desc_f.close() + + #TODO: refactor the logger in this file to follow the logging guidelines + # for 6.0 + logging.getLogger('addons').debug('The module %s does not contain a description file:'\ + '__openerp__.py or __terp__.py (deprecated)', module) + return {} + +def get_modules_with_version(): + modules = get_modules() + res = {} + for module in modules: + try: + info = load_information_from_description_file(module) + res[module] = "%s.%s" % (release.major_version, info['version']) + except Exception, e: + continue + return res + +def create_graph(cr, module_list, force=None): + graph = Graph() + upgrade_graph(graph, cr, module_list, force) + return graph + +def upgrade_graph(graph, cr, module_list, force=None): + if force is None: + force = [] + packages = [] + len_graph = len(graph) + for module in module_list: + mod_path = get_module_path(module) + terp_file = get_module_resource(module, '__openerp__.py') + if not terp_file: + terp_file = get_module_resource(module, '__terp__.py') + if not mod_path or not terp_file: + logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not found, skipped' % (module)) + continue + + if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'): + terp_f = tools.file_open(terp_file) + try: + info = eval(terp_f.read()) + except Exception: + logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: eval file %s' % (module, terp_file)) + raise + finally: + terp_f.close() + if info.get('installable', True): + packages.append((module, info.get('depends', []), info)) + else: + logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not installable, skipped' % (module)) + + dependencies = dict([(p, deps) for p, deps, data in packages]) + current, later = set([p for p, dep, data in packages]), set() + + while packages and current > later: + package, deps, data = packages[0] + + # if all dependencies of 'package' are already in the graph, add 'package' in the graph + if reduce(lambda x, y: x and y in graph, deps, True): + if not package in current: + packages.pop(0) + continue + later.clear() + current.remove(package) + graph.addNode(package, deps) + node = Node(package, graph) + node.data = data + for kind in ('init', 'demo', 'update'): + if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force: + setattr(node, kind, True) + else: + later.add(package) + packages.append((package, deps, data)) + packages.pop(0) + + graph.update_from_db(cr) + + for package in later: + unmet_deps = filter(lambda p: p not in graph, dependencies[package]) + logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: Unmet dependencies: %s' % (package, ', '.join(unmet_deps))) + + result = len(graph) - len_graph + if result != len(module_list): + logger.notifyChannel('init', netsvc.LOG_WARNING, 'Not all modules have loaded.') + return result + + +def init_module_objects(cr, module_name, obj_list): + logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: creating or updating database tables' % module_name) + todo = [] + for obj in obj_list: + try: + result = obj._auto_init(cr, {'module': module_name}) + except Exception, e: + raise + if result: + todo += result + if hasattr(obj, 'init'): + obj.init(cr) + cr.commit() + todo.sort() + for t in todo: + t[1](cr, *t[2]) + cr.commit() + + +def register_class(m): + """ + Register module named m, if not already registered + """ + + def log(e): + mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or '' + msg = "Couldn't load %smodule %s" % (mt, m) + logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg) + logger.notifyChannel('init', netsvc.LOG_CRITICAL, e) + + global loaded + if m in loaded: + return + logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m) + mod_path = get_module_path(m) + + initialize_sys_path() + try: + zip_mod_path = mod_path + '.zip' + if not os.path.isfile(zip_mod_path): + fm = imp.find_module(m, ad_paths) + try: + imp.load_module(m, *fm) + finally: + if fm[0]: + fm[0].close() + else: + zimp = zipimport.zipimporter(zip_mod_path) + zimp.load_module(m) + except Exception, e: + log(e) + raise + else: + loaded.append(m) + + +class MigrationManager(object): + """ + This class manage the migration of modules + Migrations files must be python files containing a "migrate(cr, installed_version)" function. + Theses files must respect a directory tree structure: A 'migrations' folder which containt a + folder by version. Version can be 'module' version or 'server.module' version (in this case, + the files will only be processed by this version of the server). Python file names must start + by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation + Example: + + + `-- migrations + |-- 1.0 + | |-- pre-update_table_x.py + | |-- pre-update_table_y.py + | |-- post-clean-data.py + | `-- README.txt # not processed + |-- 5.0.1.1 # files in this folder will be executed only on a 5.0 server + | |-- pre-delete_table_z.py + | `-- post-clean-data.py + `-- foo.py # not processed + + This similar structure is generated by the maintenance module with the migrations files get by + the maintenance contract + + """ + def __init__(self, cr, graph): + self.cr = cr + self.graph = graph + self.migrations = {} + self._get_files() + + def _get_files(self): + + """ + import addons.base.maintenance.utils as maintenance_utils + maintenance_utils.update_migrations_files(self.cr) + #""" + + for pkg in self.graph: + self.migrations[pkg.name] = {} + if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'): + continue + + self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {} + self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {} + + def migrate_module(self, pkg, stage): + assert stage in ('pre', 'post') + stageformat = {'pre': '[>%s]', + 'post': '[%s>]', + } + + if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'): + return + + def convert_version(version): + if version.startswith(release.major_version) and version != release.major_version: + return version # the version number already containt the server version + return "%s.%s" % (release.major_version, version) + + def _get_migration_versions(pkg): + def __get_dir(tree): + return [d for d in tree if tree[d] is not None] + + versions = list(set( + __get_dir(self.migrations[pkg.name]['module']) + + __get_dir(self.migrations[pkg.name]['maintenance']) + )) + versions.sort(key=lambda k: parse_version(convert_version(k))) + return versions + + def _get_migration_files(pkg, version, stage): + """ return a list of tuple (module, file) + """ + m = self.migrations[pkg.name] + lst = [] + + mapping = {'module': opj(pkg.name, 'migrations'), + 'maintenance': opj('base', 'maintenance', 'migrations', pkg.name), + } + + for x in mapping.keys(): + if version in m[x]: + for f in m[x][version]: + if m[x][version][f] is not None: + continue + if not f.startswith(stage + '-'): + continue + lst.append(opj(mapping[x], version, f)) + lst.sort() + return lst + + def mergedict(a, b): + a = a.copy() + a.update(b) + return a + + from openerp.tools.parse_version import parse_version + + parsed_installed_version = parse_version(pkg.installed_version or '') + current_version = parse_version(convert_version(pkg.data.get('version', '0'))) + + versions = _get_migration_versions(pkg) + + for version in versions: + if parsed_installed_version < parse_version(convert_version(version)) <= current_version: + + strfmt = {'addon': pkg.name, + 'stage': stage, + 'version': stageformat[stage] % version, + } + + for pyfile in _get_migration_files(pkg, version, stage): + name, ext = os.path.splitext(os.path.basename(pyfile)) + if ext.lower() != '.py': + continue + mod = fp = fp2 = None + try: + fp = tools.file_open(pyfile) + + # imp.load_source need a real file object, so we create + # one from the file-like object we get from file_open + fp2 = os.tmpfile() + fp2.write(fp.read()) + fp2.seek(0) + try: + mod = imp.load_source(name, pyfile, fp2) + logger.notifyChannel('migration', netsvc.LOG_INFO, 'module %(addon)s: Running migration %(version)s %(name)s' % mergedict({'name': mod.__name__}, strfmt)) + mod.migrate(self.cr, pkg.installed_version) + except ImportError: + logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % mergedict({'file': pyfile}, strfmt)) + raise + except AttributeError: + logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt) + except: + raise + finally: + if fp: + fp.close() + if fp2: + fp2.close() + if mod: + del mod + +log = logging.getLogger('init') + +def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, **kwargs): + """Migrates+Updates or Installs all module nodes from ``graph`` + :param graph: graph of module nodes to load + :param status: status dictionary for keeping track of progress + :param perform_checks: whether module descriptors should be checked for validity (prints warnings + for same cases, and even raise osv_except if certificate is invalid) + :param skip_modules: optional list of module names (packages) which have previously been loaded and can be skipped + :return: list of modules that were installed or updated + """ + def process_sql_file(cr, fp): + queries = fp.read().split(';') + for query in queries: + new_query = ' '.join(query.split()) + if new_query: + cr.execute(new_query) + + def load_init_update_xml(cr, m, idref, mode, kind): + for filename in package.data.get('%s_xml' % kind, []): + logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, filename)) + _, ext = os.path.splitext(filename) + fp = tools.file_open(opj(m, filename)) + try: + if ext == '.csv': + noupdate = (kind == 'init') + tools.convert_csv_import(cr, m, os.path.basename(filename), fp.read(), idref, mode=mode, noupdate=noupdate) + elif ext == '.sql': + process_sql_file(cr, fp) + elif ext == '.yml': + tools.convert_yaml_import(cr, m, fp, idref, mode=mode, **kwargs) + else: + tools.convert_xml_import(cr, m, fp, idref, mode=mode, **kwargs) + finally: + fp.close() + + def load_demo_xml(cr, m, idref, mode): + for xml in package.data.get('demo_xml', []): + name, ext = os.path.splitext(xml) + logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, xml)) + fp = tools.file_open(opj(m, xml)) + try: + if ext == '.csv': + tools.convert_csv_import(cr, m, os.path.basename(xml), fp.read(), idref, mode=mode, noupdate=True) + elif ext == '.yml': + tools.convert_yaml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs) + else: + tools.convert_xml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs) + finally: + fp.close() + + def load_data(cr, module_name, id_map, mode): + _load_data(cr, module_name, id_map, mode, 'data') + + def load_demo(cr, module_name, id_map, mode): + _load_data(cr, module_name, id_map, mode, 'demo') + + def load_test(cr, module_name, id_map, mode): + cr.commit() + if not tools.config.options['test_disable']: + try: + _load_data(cr, module_name, id_map, mode, 'test') + except Exception, e: + logging.getLogger('test').exception('Tests failed to execute in module %s', module_name) + finally: + if tools.config.options['test_commit']: + cr.commit() + else: + cr.rollback() + + def _load_data(cr, module_name, id_map, mode, kind): + for filename in package.data.get(kind, []): + noupdate = (kind == 'demo') + _, ext = os.path.splitext(filename) + log.info("module %s: loading %s", module_name, filename) + pathname = os.path.join(module_name, filename) + file = tools.file_open(pathname) + try: + if ext == '.sql': + process_sql_file(cr, file) + elif ext == '.csv': + noupdate = (kind == 'init') + tools.convert_csv_import(cr, module_name, pathname, file.read(), id_map, mode, noupdate) + elif ext == '.yml': + tools.convert_yaml_import(cr, module_name, file, id_map, mode, noupdate) + else: + tools.convert_xml_import(cr, module_name, file, id_map, mode, noupdate) + finally: + file.close() + + # **kwargs is passed directly to convert_xml_import + if not status: + status = {} + + status = status.copy() + processed_modules = [] + statusi = 0 + pool = pooler.get_pool(cr.dbname) + migrations = MigrationManager(cr, graph) + modobj = None + logger.notifyChannel('init', netsvc.LOG_DEBUG, 'loading %d packages..' % len(graph)) + + for package in graph: + if skip_modules and package.name in skip_modules: + continue + logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name) + migrations.migrate_module(package, 'pre') + register_class(package.name) + modules = pool.instanciate(package.name, cr) + if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'): + init_module_objects(cr, package.name, modules) + cr.commit() + + for package in graph: + status['progress'] = (float(statusi)+0.1) / len(graph) + m = package.name + mid = package.id + + if skip_modules and m in skip_modules: + continue + + if modobj is None: + modobj = pool.get('ir.module.module') + + if modobj and perform_checks: + modobj.check(cr, 1, [mid]) + + idref = {} + status['progress'] = (float(statusi)+0.4) / len(graph) + + mode = 'update' + if hasattr(package, 'init') or package.state == 'to install': + mode = 'init' + + if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'): + for kind in ('init', 'update'): + if package.state=='to upgrade': + # upgrading the module information + modobj.write(cr, 1, [mid], modobj.get_values_from_terp(package.data)) + load_init_update_xml(cr, m, idref, mode, kind) + load_data(cr, m, idref, mode) + if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'): + status['progress'] = (float(statusi)+0.75) / len(graph) + load_demo_xml(cr, m, idref, mode) + load_demo(cr, m, idref, mode) + cr.execute('update ir_module_module set demo=%s where id=%s', (True, mid)) + + # launch tests only in demo mode, as most tests will depend + # on demo data. Other tests can be added into the regular + # 'data' section, but should probably not alter the data, + # as there is no rollback. + load_test(cr, m, idref, mode) + + processed_modules.append(package.name) + + migrations.migrate_module(package, 'post') + + if modobj: + ver = release.major_version + '.' + package.data.get('version', '1.0') + # Set new modules and dependencies + modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver}) + cr.commit() + # Update translations for all installed languages + modobj.update_translations(cr, 1, [mid], None, {'overwrite': tools.config['overwrite_existing_translations']}) + cr.commit() + + package.state = 'installed' + for kind in ('init', 'demo', 'update'): + if hasattr(package, kind): + delattr(package, kind) + + statusi += 1 + + cr.commit() + + return processed_modules + +def _check_module_names(cr, module_names): + mod_names = set(module_names) + if 'base' in mod_names: + # ignore dummy 'all' module + if 'all' in mod_names: + mod_names.remove('all') + if mod_names: + cr.execute("SELECT count(id) AS count FROM ir_module_module WHERE name in %s", (tuple(mod_names),)) + if cr.dictfetchone()['count'] != len(mod_names): + # find out what module name(s) are incorrect: + cr.execute("SELECT name FROM ir_module_module") + incorrect_names = mod_names.difference([x['name'] for x in cr.dictfetchall()]) + logging.getLogger('init').warning('invalid module names, ignored: %s', ", ".join(incorrect_names)) + +def load_modules(db, force_demo=False, status=None, update_module=False): + + initialize_sys_path() + + # Backward compatibility: addons don't have to import openerp.xxx, they still can import xxx + for k, v in list(sys.modules.items()): + if k.startswith('openerp.') and sys.modules.get(k[8:]) is None: + sys.modules[k[8:]] = v + + if not status: + status = {} + cr = db.cursor() + if cr: + cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='ir_module_module'") + if len(cr.fetchall())==0: + logger.notifyChannel("init", netsvc.LOG_INFO, "init db") + tools.init_db(cr) + tools.config["init"]["all"] = 1 + tools.config['update']['all'] = 1 + if not tools.config['without_demo']: + tools.config["demo"]['all'] = 1 + force = [] + if force_demo: + force.append('demo') + + # This is a brand new pool, just created in pooler.get_db_and_pool() + pool = pooler.get_pool(cr.dbname) + + try: + processed_modules = [] + report = tools.assertion_report() + # NOTE: Try to also load the modules that have been marked as uninstallable previously... + STATES_TO_LOAD = ['installed', 'to upgrade', 'uninstallable'] + if 'base' in tools.config['update'] or 'all' in tools.config['update']: + cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed')) + + # STEP 1: LOAD BASE (must be done before module dependencies can be computed for later steps) + graph = create_graph(cr, ['base'], force) + if not graph: + logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)') + raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)')) + processed_modules.extend(load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report)) + + if tools.config['load_language']: + for lang in tools.config['load_language'].split(','): + tools.load_language(cr, lang) + + # STEP 2: Mark other modules to be loaded/updated + if update_module: + modobj = pool.get('ir.module.module') + logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list') + if ('base' in tools.config['init']) or ('base' in tools.config['update']): + modobj.update_list(cr, 1) + + _check_module_names(cr, itertools.chain(tools.config['init'].keys(), tools.config['update'].keys())) + + mods = [k for k in tools.config['init'] if tools.config['init'][k]] + if mods: + ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)]) + if ids: + modobj.button_install(cr, 1, ids) + + mods = [k for k in tools.config['update'] if tools.config['update'][k]] + if mods: + ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)]) + if ids: + modobj.button_upgrade(cr, 1, ids) + + cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base')) + + STATES_TO_LOAD += ['to install'] + + + # STEP 3: Load marked modules (skipping base which was done in STEP 1) + loop_guardrail = 0 + while True: + loop_guardrail += 1 + if loop_guardrail > 100: + raise ValueError('Possible recursive module tree detected, aborting.') + cr.execute("SELECT name from ir_module_module WHERE state IN %s" ,(tuple(STATES_TO_LOAD),)) + + module_list = [name for (name,) in cr.fetchall() if name not in graph] + if not module_list: + break + + new_modules_in_graph = upgrade_graph(graph, cr, module_list, force) + if new_modules_in_graph == 0: + # nothing to load + break + + logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list))) + processed_modules.extend(load_module_graph(cr, graph, status, report=report, skip_modules=processed_modules)) + + # load custom models + cr.execute('select model from ir_model where state=%s', ('manual',)) + for model in cr.dictfetchall(): + pool.get('ir.model').instanciate(cr, 1, model['model'], {}) + + # STEP 4: Finish and cleanup + if processed_modules: + cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""") + for (model, name) in cr.fetchall(): + model_obj = pool.get(model) + if model_obj and not isinstance(model_obj, osv.osv.osv_memory): + logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name)) + + # Temporary warning while we remove access rights on osv_memory objects, as they have + # been replaced by owner-only access rights + cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""") + for (model, name) in cr.fetchall(): + model_obj = pool.get(model) + if isinstance(model_obj, osv.osv.osv_memory): + logger.notifyChannel('init', netsvc.LOG_WARNING, 'In-memory object %s (%s) should not have explicit access rules!' % (model, name)) + + cr.execute("SELECT model from ir_model") + for (model,) in cr.fetchall(): + obj = pool.get(model) + if obj: + obj._check_removed_columns(cr, log=True) + else: + logger.notifyChannel('init', netsvc.LOG_WARNING, "Model %s is referenced but not present in the orm pool!" % model) + + # Cleanup orphan records + pool.get('ir.model.data')._process_end(cr, 1, processed_modules) + + if report.get_report(): + logger.notifyChannel('init', netsvc.LOG_INFO, report) + + for kind in ('init', 'demo', 'update'): + tools.config[kind] = {} + + cr.commit() + if update_module: + cr.execute("select id,name from ir_module_module where state=%s", ('to remove',)) + for mod_id, mod_name in cr.fetchall(): + cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,)) + for rmod, rid in cr.fetchall(): + uid = 1 + rmod_module= pool.get(rmod) + if rmod_module: + rmod_module.unlink(cr, uid, [rid]) + else: + logger.notifyChannel('init', netsvc.LOG_ERROR, 'Could not locate %s to remove res=%d' % (rmod,rid)) + cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,)) + cr.commit() + # + # TODO: remove menu without actions of children + # + while True: + cr.execute('''delete from + ir_ui_menu + where + (id not IN (select parent_id from ir_ui_menu where parent_id is not null)) + and + (id not IN (select res_id from ir_values where model='ir.ui.menu')) + and + (id not IN (select res_id from ir_model_data where model='ir.ui.menu'))''') + cr.commit() + if not cr.rowcount: + break + else: + logger.notifyChannel('init', netsvc.LOG_INFO, 'removed %d unused menus' % (cr.rowcount,)) + + cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',)) + cr.commit() + finally: + cr.close() + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/pooler.py b/openerp/pooler.py index 0de5f832494..b162bba4b8d 100644 --- a/openerp/pooler.py +++ b/openerp/pooler.py @@ -33,7 +33,7 @@ def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False, if db_name in pool_dic: pool = pool_dic[db_name] else: - import openerp.addons as addons + import openerp.modules import openerp.osv.osv as osv_osv pool = osv_osv.osv_pool() @@ -43,7 +43,7 @@ def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False, # an exception is raised. pool_dic[db_name] = pool try: - addons.load_modules(db, force_demo, status, update_module) + openerp.modules.load_modules(db, force_demo, status, update_module) except Exception: del pool_dic[db_name] raise diff --git a/openerp/report/interface.py b/openerp/report/interface.py index 5bb1224a549..178c015ba34 100644 --- a/openerp/report/interface.py +++ b/openerp/report/interface.py @@ -27,7 +27,7 @@ import openerp.netsvc as netsvc import openerp.pooler as pooler import openerp.tools as tools -import openerp.addons as addons +import openerp.modules import print_xml import render import urllib @@ -119,7 +119,7 @@ class report_rml(report_int): pos_xml = i.end() doc = print_xml.document(cr, uid, {}, {}) - tmpl_path = addons.get_module_resource('base', 'report', 'corporate_defaults.xml') + tmpl_path = openerp.modules.get_module_resource('base', 'report', 'corporate_defaults.xml') doc.parse(tmpl_path, [uid], 'res.users', context) corporate_header = doc.xml_get() doc.close() diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index 95194679198..e19fc523e3c 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -28,7 +28,7 @@ import time import sys import platform from openerp.tools.translate import _ -import openerp.addons as addons +import openerp.modules import openerp.ir import openerp.netsvc as netsvc import openerp.pooler as pooler @@ -139,7 +139,7 @@ class db(netsvc.ExportService): def exp_get_progress(self, id): if self.actions[id]['thread'].isAlive(): -# return addons.init_progress[db_name] +# return openerp.modules.init_progress[db_name] return (min(self.actions[id].get('progress', 0),0.95), []) else: clean = self.actions[id]['clean'] @@ -449,7 +449,7 @@ GNU Public Licence. if not rc.id: raise tm.RemoteContractException('This contract does not exist or is not active') - return rc.get_available_updates(rc.id, addons.get_modules_with_version()) + return rc.get_available_updates(rc.id, openerp.modules.get_modules_with_version()) except tm.RemoteContractException, e: self.abortResponse(1, 'Migration Error', 'warning', str(e)) @@ -467,7 +467,7 @@ GNU Public Licence. l.notifyChannel('migration', netsvc.LOG_INFO, 'starting migration with contract %s' % (rc.name,)) - zips = rc.retrieve_updates(rc.id, addons.get_modules_with_version()) + zips = rc.retrieve_updates(rc.id, openerp.modules.get_modules_with_version()) from shutil import rmtree, copytree, copy @@ -479,7 +479,7 @@ GNU Public Licence. for module in zips: l.notifyChannel('migration', netsvc.LOG_INFO, 'upgrade module %s' % (module,)) - mp = addons.get_module_path(module) + mp = openerp.modules.get_module_path(module) if mp: if os.path.isdir(mp): copytree(mp, os.path.join(backup_directory, module)) diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index e1a6a717e87..276274563be 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -71,7 +71,7 @@ SKIPPED_ELEMENT_TYPES = (etree._Comment, etree._ProcessingInstruction, etree.Com # initialize a database with base/base.sql def init_db(cr): - import openerp.addons as addons + import openerp.modules as addons f = addons.get_module_resource('base', 'base.sql') base_sql_file = file_open(f) try: @@ -204,7 +204,7 @@ def file_open(name, mode="r", subdir='addons', pathinfo=False): @return: fileobject if pathinfo is False else (fileobject, filepath) """ - import openerp.addons as addons + import openerp.modules as addons adps = addons.ad_paths rtp = os.path.normcase(os.path.abspath(config['root_path'])) From 2356412aea134f413e4a633e5740a793e527c47e Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 20 Apr 2011 17:37:41 +0200 Subject: [PATCH 08/35] [FIX] openerp.addons: corrected wrong import syntax. bzr revid: vmt@openerp.com-20110420153741-scgftlguxqvopgur --- openerp/addons/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openerp/addons/__init__.py b/openerp/addons/__init__.py index d019985f34c..88a0d113151 100644 --- a/openerp/addons/__init__.py +++ b/openerp/addons/__init__.py @@ -29,8 +29,7 @@ is deprecated. """ -from openerp.modules import - get_module_resource, - get_module_path # used only by base_module_quality +# get_module_path is used only by base_module_quality +from openerp.modules import get_module_resource, get_module_path # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 7008aa3e0e757b8b1a0a38e3ac0515860986a5a6 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 21 Apr 2011 12:31:25 +0200 Subject: [PATCH 09/35] [FIX] sql_db: connection pool created lazily. - This is necessary to give a chance to change the config just after importing openerp. bzr revid: vmt@openerp.com-20110421103125-3lv609wnu3vh2m4f --- openerp/sql_db.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openerp/sql_db.py b/openerp/sql_db.py index 4b1d8aab794..9b6dffad3d2 100644 --- a/openerp/sql_db.py +++ b/openerp/sql_db.py @@ -374,9 +374,12 @@ def dsn_are_equals(first, second): return key(first) == key(second) -_Pool = ConnectionPool(int(tools.config['db_maxconn'])) +_Pool = None def db_connect(db_name): + global _Pool + if _Pool is None: + _Pool = ConnectionPool(int(tools.config['db_maxconn'])) currentThread().dbname = db_name return Connection(_Pool, db_name) From a659d1fa8c65211dbe98c57156f3cd54a61b6fa9 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 21 Apr 2011 17:58:36 +0200 Subject: [PATCH 10/35] [REF] netsvc: explicit Agent thread creation. bzr revid: vmt@openerp.com-20110421155836-70da1nhpb38zqbls --- openerp-server | 2 ++ openerp/netsvc.py | 13 +++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/openerp-server b/openerp-server index a5c81d6593e..3adf5e6f574 100755 --- a/openerp-server +++ b/openerp-server @@ -150,6 +150,8 @@ if config["translate_in"]: if config["stop_after_init"]: sys.exit(0) +openerp.netsvc.start_agent() + #---------------------------------------------------------- # Launch Servers #---------------------------------------------------------- diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 9d3e338a9e1..0cc1870edb7 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -277,12 +277,13 @@ class Agent(object): time.sleep(1) time.sleep(60) -agent_runner = threading.Thread(target=Agent.runner, name="netsvc.Agent.runner") -# the agent runner is a typical daemon thread, that will never quit and must be -# terminated when the main process exits - with no consequence (the processing -# threads it spawns are not marked daemon) -agent_runner.setDaemon(True) -agent_runner.start() +def start_agent(): + agent_runner = threading.Thread(target=Agent.runner, name="netsvc.Agent.runner") + # the agent runner is a typical daemon thread, that will never quit and must be + # terminated when the main process exits - with no consequence (the processing + # threads it spawns are not marked daemon) + agent_runner.setDaemon(True) + agent_runner.start() import traceback From aaedb68331adffafeb2a93e0b65b1d68a7764e64 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 22 Apr 2011 12:07:20 +0200 Subject: [PATCH 11/35] [ADD]: dummy cache class replacement. bzr revid: vmt@openerp.com-20110422100720-5ffk1tp944b53c7a --- openerp/tools/misc.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index 276274563be..723a3a5f2ea 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -717,7 +717,28 @@ def is_hashable(h): except TypeError: return False -class cache(object): +class dummy_cache(object): + """ Cache decorator replacement to actually do no caching. + + This can be useful to benchmark and/or track memory leak. + + """ + + def __init__(self, timeout=None, skiparg=2, multi=None, size=8192): + pass + + def clear(self, dbname, *args, **kwargs): + pass + + @classmethod + def clean_caches_for_db(cls, dbname): + pass + + def __call__(self, fn): + fn.clear_cache = self.clear + return fn + +class real_cache(object): """ Use it as a decorator of the function you plan to cache Timeout: 0 = no timeout, otherwise in seconds @@ -842,6 +863,9 @@ class cache(object): cached_result.clear_cache = self.clear return cached_result +# TODO make it an option +cache = real_cache + def to_xml(s): return s.replace('&','&').replace('<','<').replace('>','>') From 2856d169128ba742a5c3985155381d5af1c651c0 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 22 Apr 2011 13:24:17 +0200 Subject: [PATCH 12/35] [ADD] sql_db: show file/line where the cursor was closed This will be printed along the error message whenever an attempt to use a closed cursor is made (only in debug_sql log level). bzr revid: vmt@openerp.com-20110422112417-dbeqq4qm59sr2wvt --- openerp/sql_db.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openerp/sql_db.py b/openerp/sql_db.py index 9b6dffad3d2..e759a13e1d3 100644 --- a/openerp/sql_db.py +++ b/openerp/sql_db.py @@ -67,17 +67,22 @@ sql_counter = 0 class Cursor(object): IN_MAX = 1000 # decent limit on size of IN queries - guideline = Oracle limit - __logger = logging.getLogger('db.cursor') + __logger = None def check(f): @wraps(f) def wrapper(self, *args, **kwargs): if self.__closed: - raise psycopg2.OperationalError('Unable to use the cursor after having closed it') + msg = 'Unable to use a closed cursor.' + if self.__closer: + msg += ' It was closed at %s, line %s' % self.__closer + raise psycopg2.OperationalError(msg) return f(self, *args, **kwargs) return wrapper def __init__(self, pool, dbname, serialized=False): + if self.__class__.__logger is None: + self.__class__.__logger = logging.getLogger('db.cursor') self.sql_from_log = {} self.sql_into_log = {} @@ -99,6 +104,7 @@ class Cursor(object): self.__caller = frame_codeinfo(currentframe(),2) else: self.__caller = False + self.__closer = False def __del__(self): if not self.__closed: @@ -197,6 +203,8 @@ class Cursor(object): if not self._obj: return + if self.sql_log: + self.__closer = frame_codeinfo(currentframe(),3) self.print_log() if not self._serialized: From 4f163f727a287663b6db499c23c189922ddf8a5b Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 27 Apr 2011 11:08:46 +0200 Subject: [PATCH 13/35] [IMP] orm: check for object _name validity. bzr revid: vmt@openerp.com-20110427090846-pxqkyk2y0p8sjb8v --- openerp/osv/orm.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index a1f32631571..85018865a3f 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -63,6 +63,36 @@ from openerp.tools.safe_eval import safe_eval as eval from openerp.tools import SKIPPED_ELEMENT_TYPES regex_order = re.compile('^(([a-z0-9_]+|"[a-z0-9_]+")( *desc| *asc)?( *, *|))+$', re.I) +regex_object_name = re.compile(r'^[a-z0-9_.]+$') + +def check_object_name(name): + """ Check if the given name is a valid openerp object name. + + The _name attribute in osv and osv_memory object is subject to + some restrictions. This function returns True or False whether + the given name is allowed or not. + + TODO: this is an approximation. The goal in this approximation + is to disallow uppercase characters (in some places, we quote + table/column names and in other not, which leads to this kind + of errors: + + psycopg2.ProgrammingError: relation "xxx" does not exist). + + The same restriction should apply to both osv and osv_memory + objects for consistency. + + """ + if regex_object_name.match(name) is None: + return False + return True + +def raise_on_invalid_object_name(name): + if not check_object_name(name): + msg = "The _name attribute %s is not valid." % name + logger = netsvc.Logger() + logger.notifyChannel('orm', netsvc.LOG_ERROR, msg) + raise except_orm('ValueError', msg) POSTGRES_CONFDELTYPES = { 'RESTRICT': 'r', @@ -508,6 +538,7 @@ class orm_template(object): cr.commit() def _auto_init(self, cr, context=None): + raise_on_invalid_object_name(self._name) self._field_create(cr, context=context) def __init__(self, cr): @@ -2363,6 +2394,7 @@ class orm(orm_template): self._table, column['attname']) def _auto_init(self, cr, context=None): + raise_on_invalid_object_name(self._name) if context is None: context = {} store_compute = False From 67aa6c461a757da0ab6c5fc9d9f5f06895a4e65b Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 27 Apr 2011 11:28:36 +0200 Subject: [PATCH 14/35] [REF] pooler: expose a delete_pool method, can be useful to debug memory leaks. bzr revid: vmt@openerp.com-20110427092836-284bci0an48nopwc --- openerp/pooler.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openerp/pooler.py b/openerp/pooler.py index b162bba4b8d..78eec82b59e 100644 --- a/openerp/pooler.py +++ b/openerp/pooler.py @@ -61,10 +61,14 @@ def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False, return db, pool -def restart_pool(db_name, force_demo=False, status=None, update_module=False): - """Delete an existing osv_pool and return a database connection and a newly initialized osv_pool.""" +def delete_pool(db_name): + """Delete an existing osv_pool.""" if db_name in pool_dic: del pool_dic[db_name] + +def restart_pool(db_name, force_demo=False, status=None, update_module=False): + """Delete an existing osv_pool and return a database connection and a newly initialized osv_pool.""" + delete_pool(db_name) return get_db_and_pool(db_name, force_demo, status, update_module=update_module) @@ -75,7 +79,6 @@ def get_db(db_name): def get_pool(db_name, force_demo=False, status=None, update_module=False): """Return an osv_pool.""" - pool = get_db_and_pool(db_name, force_demo, status, update_module)[1] - return pool + return get_db_and_pool(db_name, force_demo, status, update_module)[1] # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 4b3357652d6bae511ad54cd0f1f2b2ad554c3655 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 19 May 2011 10:50:35 +0200 Subject: [PATCH 15/35] [REF] osv_pool cleaning: - removed unused variables (module_list, module_object_list, created) - removed unecessary call to del on a dictionary (as its element is replaced anyway) - added some comments bzr revid: vmt@openerp.com-20110519085035-p9ohve0nfkudgrx5 --- openerp/osv/osv.py | 67 +++++++++++++++++++++++++++++++++------------- openerp/pooler.py | 4 ++- 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 5d2e457f965..3c9870c73b9 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -33,7 +33,7 @@ from psycopg2 import IntegrityError, errorcodes from openerp.tools.func import wraps from openerp.tools.translate import translate -module_list = [] +# Mapping between openerp module names and their osv classes. module_class_list = {} class except_osv(Exception): @@ -205,16 +205,20 @@ class object_proxy(netsvc.Service): object_proxy() class osv_pool(object): + """ Model registry for a particular database. + + The registry is essentially a mapping between model names and model + instances. There is one registry instance per database. + + """ + def __init__(self): self._ready = False - self.obj_pool = {} - self.module_object_list = {} - self.created = [] + self.obj_pool = {} # model name/model instance mapping self._sql_error = {} self._store_function = {} self._init = True self._init_parent = {} - self.logger = logging.getLogger("pool") def init_set(self, cr, mode): different = mode != self._init @@ -229,23 +233,17 @@ class osv_pool(object): self._ready = True return different - def obj_list(self): + """ Return the list of model names in this registry.""" return self.obj_pool.keys() - # adds a new object instance to the object pool. - # if it already existed, the instance is replaced - def add(self, name, obj_inst): - if name in self.obj_pool: - del self.obj_pool[name] - self.obj_pool[name] = obj_inst - module = obj_inst.__class__.__module__.split('.')[0] - self.module_object_list.setdefault(module, []).append(obj_inst) + def add(self, model_name, model): + """ Add or replace a model in the registry.""" + self.obj_pool[model_name] = model - # Return None if object does not exist def get(self, name): - obj = self.obj_pool.get(name, None) - return obj + """ Return a model for a given name or None if it doesn't exist.""" + return self.obj_pool.get(name) #TODO: pass a list of modules to load def instanciate(self, module, cr): @@ -256,18 +254,49 @@ class osv_pool(object): return res class osv_base(object): + """ Base class for openerp models. + + OpenERP models are created by inheriting from this class (although + not directly; more specifically by inheriting from osv or + osv_memory). The constructor is called once, usually directly + after the class definition, e.g.: + + class user(osv): + ... + user() + + The system will later instanciate the class once per database (on + which the class' module is installed). + + """ + def __init__(self, pool, cr): + """ Initialize a model and make it part of the given registry.""" pool.add(self._name, self) self.pool = pool super(osv_base, self).__init__(cr) def __new__(cls): + """ Register this model. + + This doesn't create an instance but simply register the model + as being part of the module where it is defined. + + TODO make it possible to not even have to call the constructor + to be registered. + + """ + + # Set the module name (e.g. base, sale, accounting, ...) on the class. module = cls.__module__.split('.')[0] if not hasattr(cls, '_module'): cls._module = module + + # Remember which models to instanciate for this module. module_class_list.setdefault(cls._module, []).append(cls) - if module not in module_list: - module_list.append(cls._module) + + # Since we don't return an instance here, the __init__ + # method won't be called. return None class osv_memory(osv_base, orm.orm_memory): diff --git a/openerp/pooler.py b/openerp/pooler.py index 78eec82b59e..84f0650b91d 100644 --- a/openerp/pooler.py +++ b/openerp/pooler.py @@ -21,6 +21,8 @@ import sql_db +# Mapping between db name and osv_pool. +# Accessed through the functions below. pool_dic = {} def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False, pooljobs=True): @@ -38,7 +40,7 @@ def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False, pool = osv_osv.osv_pool() # Initializing an osv_pool will call general code which will in turn - # call get_db_and_pool (this function) to obtain the osv_pool begin + # call get_db_and_pool (this function) to obtain the osv_pool being # initialized. Make it available in the pool_dic then remove it if # an exception is raised. pool_dic[db_name] = pool From 041a61c1b11f724eb4d548f3539b239b9e43c02a Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 20 Apr 2011 12:26:27 +0200 Subject: [PATCH 16/35] [REF] openerp.conf: new module - see its docstring for its purpose - otherwise empty for now bzr revid: vmt@openerp.com-20110420102627-j5zbn1phuc0pmffk --- openerp/__init__.py | 5 +++++ openerp/conf/__init__.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 openerp/conf/__init__.py diff --git a/openerp/__init__.py b/openerp/__init__.py index 02d16401293..a3cd4b3c00a 100644 --- a/openerp/__init__.py +++ b/openerp/__init__.py @@ -19,7 +19,12 @@ # ############################################################################## +""" OpenERP core library. + +""" + import addons +import conf import ir import loglevels import modules diff --git a/openerp/conf/__init__.py b/openerp/conf/__init__.py new file mode 100644 index 00000000000..0579025c344 --- /dev/null +++ b/openerp/conf/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2011 OpenERP s.a. (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +""" Library-wide configuration variables. + +For now, configuration code is in openerp.tools.config. It is in mainly +unprocessed form, e.g. addons_path is a string with commas-separated +paths. The aim is to have code related to configuration (command line +parsing, configuration file loading and saving, ...) in this module +and provide real Python variables, e.g. addons_paths is really a list +of paths. + +""" + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 081b6d98abd93e0d09d3fbf2cc8e3aec7db8f47f Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 28 Apr 2011 17:13:30 +0200 Subject: [PATCH 17/35] [REF] separate/expose some code. bzr revid: vmt@openerp.com-20110428151330-m8qkggoqmsry3w2z --- openerp/conf/__init__.py | 2 ++ openerp/modules/__init__.py | 31 +++++++++++++++++++++---------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/openerp/conf/__init__.py b/openerp/conf/__init__.py index 0579025c344..7de8070f9e2 100644 --- a/openerp/conf/__init__.py +++ b/openerp/conf/__init__.py @@ -30,4 +30,6 @@ of paths. """ +import deprecation + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py index 2c5c3e037fc..d4205ad9976 100644 --- a/openerp/modules/__init__.py +++ b/openerp/modules/__init__.py @@ -29,6 +29,8 @@ from os.path import join as opj import itertools import zipimport +import openerp + import openerp.osv as osv import openerp.tools as tools import openerp.tools.osutil as osutil @@ -74,6 +76,13 @@ def initialize_sys_path(): ad_paths.append(_ad) # for get_module_path +def open_openerp_namespace(): + # See comment for open_openerp_namespace. + if openerp.conf.deprecation.open_openerp_namespace: + for k, v in list(sys.modules.items()): + if k.startswith('openerp.') and sys.modules.get(k[8:]) is None: + sys.modules[k[8:]] = v + class Graph(dict): """ Modules dependency graph. @@ -457,6 +466,16 @@ def init_module_objects(cr, module_name, obj_list): cr.commit() +def load_module(module_name): + """ Load a Python module found on the addons paths.""" + fm = imp.find_module(module_name, ad_paths) + try: + imp.load_module(module_name, *fm) + finally: + if fm[0]: + fm[0].close() + + def register_class(m): """ Register module named m, if not already registered @@ -478,12 +497,7 @@ def register_class(m): try: zip_mod_path = mod_path + '.zip' if not os.path.isfile(zip_mod_path): - fm = imp.find_module(m, ad_paths) - try: - imp.load_module(m, *fm) - finally: - if fm[0]: - fm[0].close() + load_module(m) else: zimp = zipimport.zipimporter(zip_mod_path) zimp.load_module(m) @@ -834,10 +848,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False): initialize_sys_path() - # Backward compatibility: addons don't have to import openerp.xxx, they still can import xxx - for k, v in list(sys.modules.items()): - if k.startswith('openerp.') and sys.modules.get(k[8:]) is None: - sys.modules[k[8:]] = v + open_openerp_namespace() if not status: status = {} From e8df583f3b822562cf2264463fe26904ddaa0392 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 29 Apr 2011 09:50:30 +0200 Subject: [PATCH 18/35] [FIX] forgot to add new file. bzr revid: vmt@openerp.com-20110429075030-4o1vdtldc3rp3j1v --- openerp/conf/deprecation.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 openerp/conf/deprecation.py diff --git a/openerp/conf/deprecation.py b/openerp/conf/deprecation.py new file mode 100644 index 00000000000..53be31b46f9 --- /dev/null +++ b/openerp/conf/deprecation.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2011 OpenERP s.a. (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +""" Regroup variables for deprecated features. + +To keep the OpenERP server backward compatible with older modules, some +additional code is needed throughout the core library. This module keeps +track of those specific measures by providing variables that can be unset +by the user to check if her code is future proof. + +""" + +# If True, the Python modules inside the openerp namespace are made available +# without the 'openerp.' prefix. E.g. openerp.osv.osv and osv.osv refer to the +# same module. +# Introduced around 2011.02. +open_openerp_namespace = True + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 0770057935c967761962d55fd9180dcecd39664c Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Sat, 7 May 2011 13:21:29 +0200 Subject: [PATCH 19/35] [REF] explicit service objects instanciation. bzr revid: vmt@openerp.com-20110507112129-hfrly9easfby9hns --- openerp-server | 2 ++ openerp/modules/__init__.py | 2 +- openerp/netsvc.py | 13 +++++++++++++ openerp/osv/osv.py | 4 +++- openerp/report/render/__init__.py | 2 +- openerp/service/web_services.py | 13 +++++++------ 6 files changed, 27 insertions(+), 9 deletions(-) diff --git a/openerp-server b/openerp-server index 3adf5e6f574..3586b11c7d2 100755 --- a/openerp-server +++ b/openerp-server @@ -89,6 +89,8 @@ logger.info('initialising distributed objects services') if not ( config["stop_after_init"] or \ config["translate_in"] or \ config["translate_out"] ): + openerp.osv.osv.start_object_proxy() + openerp.service.web_services.start_web_services() http_server = openerp.service.http_server netrpc_server = openerp.service.netrpc_server http_server.init_servers() diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py index d4205ad9976..9a8987279c9 100644 --- a/openerp/modules/__init__.py +++ b/openerp/modules/__init__.py @@ -653,7 +653,6 @@ class MigrationManager(object): if mod: del mod -log = logging.getLogger('init') def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, **kwargs): """Migrates+Updates or Installs all module nodes from ``graph`` @@ -727,6 +726,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= for filename in package.data.get(kind, []): noupdate = (kind == 'demo') _, ext = os.path.splitext(filename) + log = logging.getLogger('init') log.info("module %s: loading %s", module_name, filename) pathname = os.path.join(module_name, filename) file = tools.file_open(pathname) diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 0cc1870edb7..9ce7f4a35d2 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -213,6 +213,19 @@ def init_logger(): logger.addHandler(handler) logger.setLevel(int(tools.config['log_level'] or '0')) +# A alternative logging scheme for automated runs of the +# server intended to test it. +def init_alternative_logger(): + class H(logging.Handler): + def emit(self, record): + if record.levelno > 20: + print record.levelno, record.pathname, record.msg + handler = H() + logger = logging.getLogger() + logger.handlers = [] + logger.addHandler(handler) + logger.setLevel(logging.ERROR) + class Agent(object): """Singleton that keeps track of cancellable tasks to run at a given timestamp. diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 3c9870c73b9..07b3869a332 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -202,7 +202,6 @@ class object_proxy(netsvc.Service): cr.close() return res -object_proxy() class osv_pool(object): """ Model registry for a particular database. @@ -383,5 +382,8 @@ class osv(osv_base, orm.orm): return obj createInstance = classmethod(createInstance) +def start_object_proxy(): + object_proxy() + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/report/render/__init__.py b/openerp/report/render/__init__.py index 1d0a4464e0b..1becd5ee514 100644 --- a/openerp/report/render/__init__.py +++ b/openerp/report/render/__init__.py @@ -27,7 +27,7 @@ try: import Image except ImportError: import logging - logging.getLogger('init').warning('Python Imaging not installed, you can use only .JPG pictures !') + logging.warning('Python Imaging not installed, you can use only .JPG pictures !') # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index e19fc523e3c..3b0d0ef7320 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -348,7 +348,6 @@ class db(netsvc.ExportService): l.notifyChannel('web-services', netsvc.LOG_ERROR, tb_s) raise return True -db() class _ObjectService(netsvc.ExportService): "A common base class for those who have fn(db, uid, password,...) " @@ -580,7 +579,6 @@ GNU Public Licence. logger.warning("Counters of SQL will not be reliable unless DEBUG_SQL is set at the server's config.") return sql_db.sql_counter -common() class objects_proxy(netsvc.ExportService): def __init__(self, name="object"): @@ -604,8 +602,6 @@ class objects_proxy(netsvc.ExportService): def new_dispatch(self,method,auth,params): pass -objects_proxy() - # # Wizard ID: 1 @@ -666,7 +662,6 @@ class wizard(netsvc.ExportService): raise Exception, 'AccessDenied' else: raise Exception, 'WizardNotFound' -wizard() # # TODO: set a maximum report number per user to avoid DOS attacks @@ -780,7 +775,13 @@ class report_spool(netsvc.ExportService): else: raise Exception, 'ReportNotFound' -report_spool() + +def start_web_services(): + db() + common() + objects_proxy() + wizard() + report_spool() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 4ba3f3c804f38056294904d83a5559258d51cac9 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 19 May 2011 11:03:00 +0200 Subject: [PATCH 20/35] [REF] osv, modules/__init__cosmetic renaming, added comments. bzr revid: vmt@openerp.com-20110519090300-bodss011798ep5cy --- openerp/modules/__init__.py | 23 +++++++++++++++++++---- openerp/osv/osv.py | 10 ++++++++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py index 9a8987279c9..ac5d8e1f544 100644 --- a/openerp/modules/__init__.py +++ b/openerp/modules/__init__.py @@ -447,11 +447,26 @@ def upgrade_graph(graph, cr, module_list, force=None): return result -def init_module_objects(cr, module_name, obj_list): - logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: creating or updating database tables' % module_name) +def init_module_models(cr, module_name, obj_list): + """ Initialize a list of models. + + Call _auto_init and init on each model to create or update the + database tables supporting the models. + + TODO better explanation of _auto_init and init. + + """ + + logger.notifyChannel('init', netsvc.LOG_INFO, + 'module %s: creating or updating database tables' % module_name) + # TODO _auto_init doesn't seem to return anything + # so this todo list would be useless. todo = [] for obj in obj_list: try: + # TODO the module in the context doesn't seem usefull: + # it is available (at least) in the class' _module attribute. + # (So module_name would be useless too.) result = obj._auto_init(cr, {'module': module_name}) except Exception, e: raise @@ -761,9 +776,9 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name) migrations.migrate_module(package, 'pre') register_class(package.name) - modules = pool.instanciate(package.name, cr) + models = pool.instanciate(package.name, cr) if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'): - init_module_objects(cr, package.name, modules) + init_module_models(cr, package.name, models) cr.commit() for package in graph: diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 07b3869a332..c7cc4de5128 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -23,6 +23,8 @@ # OSV: Objects Services # +import sys +import inspect import orm import openerp.netsvc as netsvc import openerp.pooler as pooler @@ -246,10 +248,14 @@ class osv_pool(object): #TODO: pass a list of modules to load def instanciate(self, module, cr): + """ Instanciate all the classes of a given module for a particular db.""" + res = [] - class_list = module_class_list.get(module, []) - for klass in class_list: + + # instanciate classes registered through their constructor + for klass in module_class_list.get(module, []): res.append(klass.createInstance(self, module, cr)) + return res class osv_base(object): From 7963da3f940ca201fc00b64f77a7a9985d33b468 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 10 May 2011 11:49:08 +0200 Subject: [PATCH 21/35] [REF] modules.__init__: - combined code from load_init_update_xml and load_demo_xml - made explicit the "report" kwarg of load_module_graph bzr revid: vmt@openerp.com-20110510094908-txzj3oeypz7iralk --- openerp/modules/__init__.py | 71 ++++++++++++++++++------------------ openerp/tools/yaml_import.py | 2 +- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py index ac5d8e1f544..d3a4a0ba543 100644 --- a/openerp/modules/__init__.py +++ b/openerp/modules/__init__.py @@ -669,7 +669,7 @@ class MigrationManager(object): del mod -def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, **kwargs): +def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, report=None): """Migrates+Updates or Installs all module nodes from ``graph`` :param graph: graph of module nodes to load :param status: status dictionary for keeping track of progress @@ -685,38 +685,14 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= if new_query: cr.execute(new_query) - def load_init_update_xml(cr, m, idref, mode, kind): - for filename in package.data.get('%s_xml' % kind, []): - logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, filename)) - _, ext = os.path.splitext(filename) - fp = tools.file_open(opj(m, filename)) - try: - if ext == '.csv': - noupdate = (kind == 'init') - tools.convert_csv_import(cr, m, os.path.basename(filename), fp.read(), idref, mode=mode, noupdate=noupdate) - elif ext == '.sql': - process_sql_file(cr, fp) - elif ext == '.yml': - tools.convert_yaml_import(cr, m, fp, idref, mode=mode, **kwargs) - else: - tools.convert_xml_import(cr, m, fp, idref, mode=mode, **kwargs) - finally: - fp.close() + def load_init_xml(cr, m, idref, mode): + _load_xml(cr, m, idref, mode, 'init') + + def load_update_xml(cr, m, idref, mode): + _load_xml(cr, m, idref, mode, 'update') def load_demo_xml(cr, m, idref, mode): - for xml in package.data.get('demo_xml', []): - name, ext = os.path.splitext(xml) - logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, xml)) - fp = tools.file_open(opj(m, xml)) - try: - if ext == '.csv': - tools.convert_csv_import(cr, m, os.path.basename(xml), fp.read(), idref, mode=mode, noupdate=True) - elif ext == '.yml': - tools.convert_yaml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs) - else: - tools.convert_xml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs) - finally: - fp.close() + _load_xml(cr, m, idref, mode, 'demo') def load_data(cr, module_name, id_map, mode): _load_data(cr, module_name, id_map, mode, 'data') @@ -737,6 +713,29 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= else: cr.rollback() + def _load_xml(cr, m, idref, mode, kind): + for filename in package.data.get('%s_xml' % kind, []): + logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, filename)) + _, ext = os.path.splitext(filename) + fp = tools.file_open(opj(m, filename)) + noupdate = False + if kind == 'demo': + noupdate = True + try: + if ext == '.csv': + if kind == 'init': + noupdate = True + # i.e. noupdate == False when kind == 'update' + tools.convert_csv_import(cr, m, os.path.basename(filename), fp.read(), idref, mode=mode, noupdate=noupdate) + elif kind != 'demo' and ext == '.sql': + process_sql_file(cr, fp) + elif ext == '.yml': + tools.convert_yaml_import(cr, m, fp, idref, mode=mode, noupdate=noupdate) + else: + tools.convert_xml_import(cr, m, fp, idref, mode=mode, noupdate=noupdate, report=report) + finally: + fp.close() + def _load_data(cr, module_name, id_map, mode, kind): for filename in package.data.get(kind, []): noupdate = (kind == 'demo') @@ -803,11 +802,11 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= mode = 'init' if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'): - for kind in ('init', 'update'): - if package.state=='to upgrade': - # upgrading the module information - modobj.write(cr, 1, [mid], modobj.get_values_from_terp(package.data)) - load_init_update_xml(cr, m, idref, mode, kind) + if package.state=='to upgrade': + # upgrading the module information + modobj.write(cr, 1, [mid], modobj.get_values_from_terp(package.data)) + load_init_xml(cr, m, idref, mode) + load_update_xml(cr, m, idref, mode) load_data(cr, m, idref, mode) if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'): status['progress'] = (float(statusi)+0.75) / len(graph) diff --git a/openerp/tools/yaml_import.py b/openerp/tools/yaml_import.py index 06d9526d77e..2b6c0815e0d 100644 --- a/openerp/tools/yaml_import.py +++ b/openerp/tools/yaml_import.py @@ -792,7 +792,7 @@ class YamlInterpreter(object): is_preceded_by_comment = False return is_preceded_by_comment -def yaml_import(cr, module, yamlfile, idref=None, mode='init', noupdate=False, report=None): +def yaml_import(cr, module, yamlfile, idref=None, mode='init', noupdate=False): if idref is None: idref = {} yaml_string = yamlfile.read() From e82aeba808556b979a1c0ce017ce32e5df79ab79 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 19 May 2011 11:04:10 +0200 Subject: [PATCH 22/35] [REF] it seems ok to get the ir.module.module model only once. bzr revid: vmt@openerp.com-20110519090410-a89n7ivs2nvn60bt --- openerp/modules/__init__.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py index d3a4a0ba543..9d99da9a627 100644 --- a/openerp/modules/__init__.py +++ b/openerp/modules/__init__.py @@ -766,7 +766,6 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= statusi = 0 pool = pooler.get_pool(cr.dbname) migrations = MigrationManager(cr, graph) - modobj = None logger.notifyChannel('init', netsvc.LOG_DEBUG, 'loading %d packages..' % len(graph)) for package in graph: @@ -780,6 +779,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= init_module_models(cr, package.name, models) cr.commit() + modobj = pool.get('ir.module.module') for package in graph: status['progress'] = (float(statusi)+0.1) / len(graph) m = package.name @@ -788,10 +788,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= if skip_modules and m in skip_modules: continue - if modobj is None: - modobj = pool.get('ir.module.module') - - if modobj and perform_checks: + if perform_checks: modobj.check(cr, 1, [mid]) idref = {} @@ -824,14 +821,13 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= migrations.migrate_module(package, 'post') - if modobj: - ver = release.major_version + '.' + package.data.get('version', '1.0') - # Set new modules and dependencies - modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver}) - cr.commit() - # Update translations for all installed languages - modobj.update_translations(cr, 1, [mid], None, {'overwrite': tools.config['overwrite_existing_translations']}) - cr.commit() + ver = release.major_version + '.' + package.data.get('version', '1.0') + # Set new modules and dependencies + modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver}) + cr.commit() + # Update translations for all installed languages + modobj.update_translations(cr, 1, [mid], None) + cr.commit() package.state = 'installed' for kind in ('init', 'demo', 'update'): From 853e354b553162ee8c12b7f6e4ee5be51a1a6241 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 10 May 2011 14:52:42 +0200 Subject: [PATCH 23/35] [REF] modules.__init__: combined _load_data and _load_xml: - pass the report argument to xml_import - allow .sql in demo - noupdate is True for both init and demo - the pathname given to convert_csv_import is different but the way it is processed in convert_csv_import should make it alright. bzr revid: vmt@openerp.com-20110510125242-ig74vtich2g3bjhc --- openerp/modules/__init__.py | 71 +++++++++++++++---------------------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py index 9d99da9a627..45fc0ee3e80 100644 --- a/openerp/modules/__init__.py +++ b/openerp/modules/__init__.py @@ -686,25 +686,25 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= cr.execute(new_query) def load_init_xml(cr, m, idref, mode): - _load_xml(cr, m, idref, mode, 'init') + _load_data(cr, m, idref, mode, 'init_xml') def load_update_xml(cr, m, idref, mode): - _load_xml(cr, m, idref, mode, 'update') + _load_data(cr, m, idref, mode, 'update_xml') def load_demo_xml(cr, m, idref, mode): - _load_xml(cr, m, idref, mode, 'demo') + _load_data(cr, m, idref, mode, 'demo_xml') - def load_data(cr, module_name, id_map, mode): - _load_data(cr, module_name, id_map, mode, 'data') + def load_data(cr, module_name, idref, mode): + _load_data(cr, module_name, idref, mode, 'data') - def load_demo(cr, module_name, id_map, mode): - _load_data(cr, module_name, id_map, mode, 'demo') + def load_demo(cr, module_name, idref, mode): + _load_data(cr, module_name, idref, mode, 'demo') - def load_test(cr, module_name, id_map, mode): + def load_test(cr, module_name, idref, mode): cr.commit() if not tools.config.options['test_disable']: try: - _load_data(cr, module_name, id_map, mode, 'test') + _load_data(cr, module_name, idref, mode, 'test') except Exception, e: logging.getLogger('test').exception('Tests failed to execute in module %s', module_name) finally: @@ -713,51 +713,38 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= else: cr.rollback() - def _load_xml(cr, m, idref, mode, kind): - for filename in package.data.get('%s_xml' % kind, []): - logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, filename)) + def _load_data(cr, module_name, idref, mode, kind): + """ + + kind: data, demo, test, init_xml, update_xml, demo_xml. + + noupdate is False, unless it is demo data or it is csv data in + init mode. + + """ + for filename in package.data.get(kind, []): + log = logging.getLogger('init') + log.info("module %s: loading %s", module_name, filename) _, ext = os.path.splitext(filename) - fp = tools.file_open(opj(m, filename)) + pathname = os.path.join(module_name, filename) + fp = tools.file_open(pathname) noupdate = False - if kind == 'demo': + if kind in ('demo', 'demo_xml'): noupdate = True try: if ext == '.csv': - if kind == 'init': + if kind in ('init', 'init_xml'): noupdate = True - # i.e. noupdate == False when kind == 'update' - tools.convert_csv_import(cr, m, os.path.basename(filename), fp.read(), idref, mode=mode, noupdate=noupdate) - elif kind != 'demo' and ext == '.sql': + tools.convert_csv_import(cr, module_name, pathname, fp.read(), idref, mode, noupdate) + elif ext == '.sql': process_sql_file(cr, fp) elif ext == '.yml': - tools.convert_yaml_import(cr, m, fp, idref, mode=mode, noupdate=noupdate) + tools.convert_yaml_import(cr, module_name, fp, idref, mode, noupdate) else: - tools.convert_xml_import(cr, m, fp, idref, mode=mode, noupdate=noupdate, report=report) + tools.convert_xml_import(cr, module_name, fp, idref, mode, noupdate, report) finally: fp.close() - def _load_data(cr, module_name, id_map, mode, kind): - for filename in package.data.get(kind, []): - noupdate = (kind == 'demo') - _, ext = os.path.splitext(filename) - log = logging.getLogger('init') - log.info("module %s: loading %s", module_name, filename) - pathname = os.path.join(module_name, filename) - file = tools.file_open(pathname) - try: - if ext == '.sql': - process_sql_file(cr, file) - elif ext == '.csv': - noupdate = (kind == 'init') - tools.convert_csv_import(cr, module_name, pathname, file.read(), id_map, mode, noupdate) - elif ext == '.yml': - tools.convert_yaml_import(cr, module_name, file, id_map, mode, noupdate) - else: - tools.convert_xml_import(cr, module_name, file, id_map, mode, noupdate) - finally: - file.close() - - # **kwargs is passed directly to convert_xml_import if not status: status = {} From 71de92dc44a89b7a7402c1f28d3a528fde3d24cf Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 10 May 2011 16:28:49 +0200 Subject: [PATCH 24/35] [FIX] db.get_progress was broken: - the status dictionary was ignored - it is still slightly broken (see added comment in openerp/modules/__init__.py) bzr revid: vmt@openerp.com-20110510142849-p9kcd60agybpm8bd --- openerp/modules/__init__.py | 8 ++++---- openerp/pooler.py | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py index 45fc0ee3e80..aee0ceebf13 100644 --- a/openerp/modules/__init__.py +++ b/openerp/modules/__init__.py @@ -745,10 +745,9 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= finally: fp.close() - if not status: + if status is None: status = {} - status = status.copy() processed_modules = [] statusi = 0 pool = pooler.get_pool(cr.dbname) @@ -842,13 +841,14 @@ def _check_module_names(cr, module_names): logging.getLogger('init').warning('invalid module names, ignored: %s', ", ".join(incorrect_names)) def load_modules(db, force_demo=False, status=None, update_module=False): + # TODO status['progress'] reporting is broken: used twice (and reset each + # time to zero) in load_module_graph, not fine-grained enough. + # It should be a method exposed by the pool. initialize_sys_path() open_openerp_namespace() - if not status: - status = {} cr = db.cursor() if cr: cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='ir_module_module'") diff --git a/openerp/pooler.py b/openerp/pooler.py index 84f0650b91d..fb47a948752 100644 --- a/openerp/pooler.py +++ b/openerp/pooler.py @@ -27,8 +27,6 @@ pool_dic = {} def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False, pooljobs=True): """Return a database connection and an initialized osv_pool.""" - if not status: - status={} db = sql_db.db_connect(db_name) From 8b878de82d5d31fb1bcb862a2187a40ed1a24b18 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 11 May 2011 12:34:55 +0200 Subject: [PATCH 25/35] [REF] modules.__init__: - added some comments - the "updating modules list" log appears only when actually updating the list - similar code to read the module descriptor file is combined - default values for some descriptor entries are given only once - in particular, the default version is always 0, instead of 0 when reading a descriptor for migration and 1.0 when writing its value in database. bzr revid: vmt@openerp.com-20110511103455-lqezcrjf2996j7mc --- openerp/modules/__init__.py | 92 +++++++++++++++++++++---------------- openerp/tools/misc.py | 1 + tools/module_graph.py | 1 + 3 files changed, 54 insertions(+), 40 deletions(-) diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py index aee0ceebf13..44d19e48342 100644 --- a/openerp/modules/__init__.py +++ b/openerp/modules/__init__.py @@ -352,21 +352,39 @@ def load_information_from_description_file(module): :param module: The name of the module (sale, purchase, ...) """ - for filename in ['__openerp__.py', '__terp__.py']: - description_file = get_module_resource(module, filename) - if description_file : - desc_f = tools.file_open(description_file) + terp_file = get_module_resource(module, '__openerp__.py') + if not terp_file: + terp_file = get_module_resource(module, '__terp__.py') + mod_path = get_module_path(module) + if terp_file: + info = {} + if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'): + terp_f = tools.file_open(terp_file) try: - return eval(desc_f.read()) + info = eval(terp_f.read()) + except Exception: + logger.notifyChannel('modules', netsvc.LOG_ERROR, + 'module %s: exception while evaluating file %s' % + (module, terp_file)) + raise finally: - desc_f.close() + terp_f.close() + # TODO the version should probably be mandatory + info.setdefault('version', '0') + info.setdefault('depends', []) + info.setdefault('installable', True) + for kind in ['data', 'demo', 'test', + 'init_xml', 'update_xml', 'demo_xml']: + info.setdefault(kind, []) + return info #TODO: refactor the logger in this file to follow the logging guidelines # for 6.0 - logging.getLogger('addons').debug('The module %s does not contain a description file:'\ - '__openerp__.py or __terp__.py (deprecated)', module) + logging.getLogger('modules').debug('module %s: no descriptor file' + ' found: __openerp__.py or __terp__.py (deprecated)', module) return {} + def get_modules_with_version(): modules = get_modules() res = {} @@ -389,33 +407,19 @@ def upgrade_graph(graph, cr, module_list, force=None): packages = [] len_graph = len(graph) for module in module_list: - mod_path = get_module_path(module) - terp_file = get_module_resource(module, '__openerp__.py') - if not terp_file: - terp_file = get_module_resource(module, '__terp__.py') - if not mod_path or not terp_file: - logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not found, skipped' % (module)) - continue + # This will raise an exception if no/unreadable descriptor file. + info = load_information_from_description_file(module) + if info['installable']: + packages.append((module, info)) # TODO directly a dict, like in get_modules_with_version + else: + logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not installable, skipped' % (module)) - if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'): - terp_f = tools.file_open(terp_file) - try: - info = eval(terp_f.read()) - except Exception: - logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: eval file %s' % (module, terp_file)) - raise - finally: - terp_f.close() - if info.get('installable', True): - packages.append((module, info.get('depends', []), info)) - else: - logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not installable, skipped' % (module)) - - dependencies = dict([(p, deps) for p, deps, data in packages]) - current, later = set([p for p, dep, data in packages]), set() + dependencies = dict([(p, info['depends']) for p, info in packages]) + current, later = set([p for p, info in packages]), set() while packages and current > later: - package, deps, data = packages[0] + package, info = packages[0] + deps = info['depends'] # if all dependencies of 'package' are already in the graph, add 'package' in the graph if reduce(lambda x, y: x and y in graph, deps, True): @@ -426,13 +430,13 @@ def upgrade_graph(graph, cr, module_list, force=None): current.remove(package) graph.addNode(package, deps) node = Node(package, graph) - node.data = data + node.data = info for kind in ('init', 'demo', 'update'): if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force: setattr(node, kind, True) else: later.add(package) - packages.append((package, deps, data)) + packages.append((package, info)) packages.pop(0) graph.update_from_db(cr) @@ -624,7 +628,7 @@ class MigrationManager(object): from openerp.tools.parse_version import parse_version parsed_installed_version = parse_version(pkg.installed_version or '') - current_version = parse_version(convert_version(pkg.data.get('version', '0'))) + current_version = parse_version(convert_version(pkg.data['version'])) versions = _get_migration_versions(pkg) @@ -722,7 +726,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= init mode. """ - for filename in package.data.get(kind, []): + for filename in package.data[kind]: log = logging.getLogger('init') log.info("module %s: loading %s", module_name, filename) _, ext = os.path.splitext(filename) @@ -754,6 +758,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= migrations = MigrationManager(cr, graph) logger.notifyChannel('init', netsvc.LOG_DEBUG, 'loading %d packages..' % len(graph)) + # register, instanciate and initialize models for each modules for package in graph: if skip_modules and package.name in skip_modules: continue @@ -765,6 +770,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= init_module_models(cr, package.name, models) cr.commit() + # load data for each modules modobj = pool.get('ir.module.module') for package in graph: status['progress'] = (float(statusi)+0.1) / len(graph) @@ -807,7 +813,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= migrations.migrate_module(package, 'post') - ver = release.major_version + '.' + package.data.get('version', '1.0') + ver = release.major_version + '.' + package.data['version'] # Set new modules and dependencies modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver}) cr.commit() @@ -888,8 +894,8 @@ def load_modules(db, force_demo=False, status=None, update_module=False): # STEP 2: Mark other modules to be loaded/updated if update_module: modobj = pool.get('ir.module.module') - logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list') if ('base' in tools.config['init']) or ('base' in tools.config['update']): + logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list') modobj.update_list(cr, 1) _check_module_names(cr, itertools.chain(tools.config['init'].keys(), tools.config['update'].keys())) @@ -971,6 +977,8 @@ def load_modules(db, force_demo=False, status=None, update_module=False): cr.commit() if update_module: + # Remove records referenced from ir_model_data for modules to be + # removed (and removed the references from ir_model_data). cr.execute("select id,name from ir_module_module where state=%s", ('to remove',)) for mod_id, mod_name in cr.fetchall(): cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,)) @@ -978,14 +986,17 @@ def load_modules(db, force_demo=False, status=None, update_module=False): uid = 1 rmod_module= pool.get(rmod) if rmod_module: + # TODO group by module so that we can delete multiple ids in a call rmod_module.unlink(cr, uid, [rid]) else: logger.notifyChannel('init', netsvc.LOG_ERROR, 'Could not locate %s to remove res=%d' % (rmod,rid)) cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,)) cr.commit() - # + + # Remove menu items that are not referenced by any of other + # (child) menu item, ir_values, or ir_model_data. + # This code could be a method of ir_ui_menu. # TODO: remove menu without actions of children - # while True: cr.execute('''delete from ir_ui_menu @@ -1001,6 +1012,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False): else: logger.notifyChannel('init', netsvc.LOG_INFO, 'removed %d unused menus' % (cr.rowcount,)) + # Pretend that modules to be removed are actually uninstalled. cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',)) cr.commit() finally: diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index 723a3a5f2ea..1c1fbe0f1fa 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -85,6 +85,7 @@ def init_db(cr): if not mod_path: continue + # This will raise an exception if no/unreadable descriptor file. info = addons.load_information_from_description_file(i) if not info: diff --git a/tools/module_graph.py b/tools/module_graph.py index 05bb058a447..b29bd316e2f 100755 --- a/tools/module_graph.py +++ b/tools/module_graph.py @@ -27,6 +27,7 @@ import optparse import sys import glob +# TODO use the same function provided in openerp.modules def load_information_from_description_file(module): """ :param module: The name of the module (sale, purchase, ...) From b158805af7d157a70af9c87a8f30abf065ac7036 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 11 May 2011 15:53:32 +0200 Subject: [PATCH 26/35] [REF] module info (descriptor) dictionary default values in one place. bzr revid: vmt@openerp.com-20110511135332-87kd5vo4rdeb6b3l --- openerp/addons/base/module/module.py | 3 +-- openerp/modules/__init__.py | 9 +++++++++ openerp/tools/misc.py | 20 +++++++++----------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/openerp/addons/base/module/module.py b/openerp/addons/base/module/module.py index 36a5f80dfa3..51fdb224918 100644 --- a/openerp/addons/base/module/module.py +++ b/openerp/addons/base/module/module.py @@ -79,8 +79,7 @@ class module(osv.osv): info = {} try: info = addons.load_information_from_description_file(name) - if 'version' in info: - info['version'] = release.major_version + '.' + info['version'] + info['version'] = release.major_version + '.' + info['version'] except Exception: cls.__logger.debug('Error when trying to fetch informations for ' 'module %s', name, exc_info=True) diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py index 44d19e48342..a07248f786f 100644 --- a/openerp/modules/__init__.py +++ b/openerp/modules/__init__.py @@ -371,8 +371,17 @@ def load_information_from_description_file(module): terp_f.close() # TODO the version should probably be mandatory info.setdefault('version', '0') + info.setdefault('category', 'Uncategorized') info.setdefault('depends', []) + info.setdefault('author', '') + info.setdefault('website', '') + info.setdefault('name', False) + info.setdefault('description', '') + info['certificate'] = info.get('certificate') or None + info['web'] = info.get('web') or False + info['license'] = info.get('license') or 'AGPL-3' info.setdefault('installable', True) + info.setdefault('active', False) for kind in ['data', 'demo', 'test', 'init_xml', 'update_xml', 'demo_xml']: info.setdefault(kind, []) diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index 1c1fbe0f1fa..fd31bc14f20 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -90,7 +90,7 @@ def init_db(cr): if not info: continue - categs = info.get('category', 'Uncategorized').split('/') + categs = info['category'].split('/') p_id = None while categs: if p_id is not None: @@ -112,10 +112,8 @@ def init_db(cr): p_id = c_id categs = categs[1:] - active = info.get('active', False) - installable = info.get('installable', True) - if installable: - if active: + if info['installable']: + if info['active']: state = 'to install' else: state = 'uninstalled' @@ -125,16 +123,16 @@ def init_db(cr): (author, website, name, shortdesc, description, \ category_id, state, certificate, web, license) \ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id', ( - info.get('author', ''), - info.get('website', ''), i, info.get('name', False), - info.get('description', ''), p_id, state, info.get('certificate') or None, - info.get('web') or False, - info.get('license') or 'AGPL-3')) + info['author'], + info['website'], i, info['name'], + info['description'], p_id, state, info['certificate'], + info['web'], + info['license'])) id = cr.fetchone()[0] cr.execute('INSERT INTO ir_model_data \ (name,model,module, res_id, noupdate) VALUES (%s,%s,%s,%s,%s)', ( 'module_meta_information', 'ir.module.module', i, id, True)) - dependencies = info.get('depends', []) + dependencies = info['depends'] for d in dependencies: cr.execute('INSERT INTO ir_module_module_dependency \ (module_id,name) VALUES (%s, %s)', (id, d)) From 5ca0f64b4e80e9d01fe231bbd75bf9c686a7b44f Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 11 May 2011 16:47:54 +0200 Subject: [PATCH 27/35] [REF] moved init_db from tools.misc to modules.db. bzr revid: vmt@openerp.com-20110511144754-m06exg4dsn2jbdgz --- openerp/modules/__init__.py | 18 +++-- openerp/modules/db.py | 112 ++++++++++++++++++++++++++++++++ openerp/service/web_services.py | 3 +- openerp/tools/misc.py | 69 -------------------- 4 files changed, 127 insertions(+), 75 deletions(-) create mode 100644 openerp/modules/db.py diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py index a07248f786f..2db494754eb 100644 --- a/openerp/modules/__init__.py +++ b/openerp/modules/__init__.py @@ -50,6 +50,8 @@ from cStringIO import StringIO import logging +import openerp.modules.db + logger = netsvc.Logger() _ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base) @@ -417,6 +419,8 @@ def upgrade_graph(graph, cr, module_list, force=None): len_graph = len(graph) for module in module_list: # This will raise an exception if no/unreadable descriptor file. + # NOTE The call to load_information_from_description_file is already + # done by db.initialize, so it is possible to not do it again here. info = load_information_from_description_file(module) if info['installable']: packages.append((module, info)) # TODO directly a dict, like in get_modules_with_version @@ -504,9 +508,13 @@ def load_module(module_name): fm[0].close() -def register_class(m): - """ - Register module named m, if not already registered +def register_module_classes(m): + """ Register module named m, if not already registered. + + This will load the module and register all of its models. (Actually, the + explicit constructor call of each of the models inside the module will + register them.) + """ def log(e): @@ -773,7 +781,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= continue logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name) migrations.migrate_module(package, 'pre') - register_class(package.name) + register_module_classes(package.name) models = pool.instanciate(package.name, cr) if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'): init_module_models(cr, package.name, models) @@ -869,7 +877,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False): cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='ir_module_module'") if len(cr.fetchall())==0: logger.notifyChannel("init", netsvc.LOG_INFO, "init db") - tools.init_db(cr) + openerp.modules.db.initialize(cr) tools.config["init"]["all"] = 1 tools.config['update']['all'] = 1 if not tools.config['without_demo']: diff --git a/openerp/modules/db.py b/openerp/modules/db.py new file mode 100644 index 00000000000..261b39ffb31 --- /dev/null +++ b/openerp/modules/db.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# Copyright (C) 2010 OpenERP s.a. (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import openerp.modules + +def initialize(cr): + """ Initialize a database with for the ORM. + + This executes base/base.sql, creates the ir_module_categories (taken + from each module descriptor file), and creates the ir_module_module + and ir_model_data entries. + + """ + f = openerp.modules.get_module_resource('base', 'base.sql') + base_sql_file = openerp.tools.misc.file_open(f) + try: + cr.execute(base_sql_file.read()) + cr.commit() + finally: + base_sql_file.close() + + for i in openerp.modules.get_modules(): + mod_path = openerp.modules.get_module_path(i) + if not mod_path: + continue + + # This will raise an exception if no/unreadable descriptor file. + info = openerp.modules.load_information_from_description_file(i) + + if not info: + continue + categories = info['category'].split('/') + category_id = create_categories(cr, categories) + + if info['installable']: + if info['active']: + state = 'to install' + else: + state = 'uninstalled' + else: + state = 'uninstallable' + + cr.execute('INSERT INTO ir_module_module \ + (author, website, name, shortdesc, description, \ + category_id, state, certificate, web, license) \ + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id', ( + info['author'], + info['website'], i, info['name'], + info['description'], category_id, state, info['certificate'], + info['web'], + info['license'])) + id = cr.fetchone()[0] + cr.execute('INSERT INTO ir_model_data \ + (name,model,module, res_id, noupdate) VALUES (%s,%s,%s,%s,%s)', ( + 'module_meta_information', 'ir.module.module', i, id, True)) + dependencies = info['depends'] + for d in dependencies: + cr.execute('INSERT INTO ir_module_module_dependency \ + (module_id,name) VALUES (%s, %s)', (id, d)) + cr.commit() + +def create_categories(cr, categories): + """ Create the ir_module_category entries for some categories. + + categories is a list of strings forming a single category with its + parent categories, like ['Grand Parent', 'Parent', 'Child']. + + Return the database id of the (last) category. + + """ + p_id = None + while categories: + if p_id is not None: + cr.execute('SELECT id \ + FROM ir_module_category \ + WHERE name=%s AND parent_id=%s', (categories[0], p_id)) + else: + cr.execute('SELECT id \ + FROM ir_module_category \ + WHERE name=%s AND parent_id IS NULL', (categories[0],)) + c_id = cr.fetchone() + if not c_id: + cr.execute('INSERT INTO ir_module_category \ + (name, parent_id) \ + VALUES (%s, %s) RETURNING id', (categories[0], p_id)) + c_id = cr.fetchone()[0] + else: + c_id = c_id[0] + p_id = c_id + categories = categories[1:] + return p_id + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index 3b0d0ef7320..8806e1b4f99 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -35,6 +35,7 @@ import openerp.pooler as pooler import openerp.release as release import openerp.sql_db as sql_db import openerp.tools as tools +import openerp.modules import locale import logging from cStringIO import StringIO @@ -92,7 +93,7 @@ class db(netsvc.ExportService): try: serv.actions[id]['progress'] = 0 cr = sql_db.db_connect(db_name).cursor() - tools.init_db(cr) + openerp.modules.db.initialize(cr) tools.config['lang'] = lang cr.commit() cr.close() diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index fd31bc14f20..f232dd4ceac 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -69,75 +69,6 @@ _logger = logging.getLogger('tools') # We include the *Base ones just in case, currently they seem to be subclasses of the _* ones. SKIPPED_ELEMENT_TYPES = (etree._Comment, etree._ProcessingInstruction, etree.CommentBase, etree.PIBase) -# initialize a database with base/base.sql -def init_db(cr): - import openerp.modules as addons - f = addons.get_module_resource('base', 'base.sql') - base_sql_file = file_open(f) - try: - cr.execute(base_sql_file.read()) - cr.commit() - finally: - base_sql_file.close() - - for i in addons.get_modules(): - mod_path = addons.get_module_path(i) - if not mod_path: - continue - - # This will raise an exception if no/unreadable descriptor file. - info = addons.load_information_from_description_file(i) - - if not info: - continue - categs = info['category'].split('/') - p_id = None - while categs: - if p_id is not None: - cr.execute('SELECT id \ - FROM ir_module_category \ - WHERE name=%s AND parent_id=%s', (categs[0], p_id)) - else: - cr.execute('SELECT id \ - FROM ir_module_category \ - WHERE name=%s AND parent_id IS NULL', (categs[0],)) - c_id = cr.fetchone() - if not c_id: - cr.execute('INSERT INTO ir_module_category \ - (name, parent_id) \ - VALUES (%s, %s) RETURNING id', (categs[0], p_id)) - c_id = cr.fetchone()[0] - else: - c_id = c_id[0] - p_id = c_id - categs = categs[1:] - - if info['installable']: - if info['active']: - state = 'to install' - else: - state = 'uninstalled' - else: - state = 'uninstallable' - cr.execute('INSERT INTO ir_module_module \ - (author, website, name, shortdesc, description, \ - category_id, state, certificate, web, license) \ - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id', ( - info['author'], - info['website'], i, info['name'], - info['description'], p_id, state, info['certificate'], - info['web'], - info['license'])) - id = cr.fetchone()[0] - cr.execute('INSERT INTO ir_model_data \ - (name,model,module, res_id, noupdate) VALUES (%s,%s,%s,%s,%s)', ( - 'module_meta_information', 'ir.module.module', i, id, True)) - dependencies = info['depends'] - for d in dependencies: - cr.execute('INSERT INTO ir_module_module_dependency \ - (module_id,name) VALUES (%s, %s)', (id, d)) - cr.commit() - def find_in_path(name): try: return which(name) From 52d129150631685edd98cb21f90d53f9bfe4ae5e Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 11 May 2011 17:28:10 +0200 Subject: [PATCH 28/35] [REF] moved graph code from modules.__init__ to modules.graph - made upgrade_graph a method of Graph called add_modules - removed unnecessary create_graph function. bzr revid: vmt@openerp.com-20110511152810-etqru9pct1zwyqno --- openerp/modules/__init__.py | 166 +--------------------------- openerp/modules/graph.py | 215 ++++++++++++++++++++++++++++++++++++ 2 files changed, 219 insertions(+), 162 deletions(-) create mode 100644 openerp/modules/graph.py diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py index 2db494754eb..539abc6553f 100644 --- a/openerp/modules/__init__.py +++ b/openerp/modules/__init__.py @@ -51,6 +51,7 @@ from cStringIO import StringIO import logging import openerp.modules.db +import openerp.modules.graph logger = netsvc.Logger() @@ -85,110 +86,6 @@ def open_openerp_namespace(): if k.startswith('openerp.') and sys.modules.get(k[8:]) is None: sys.modules[k[8:]] = v -class Graph(dict): - """ Modules dependency graph. - - The graph is a mapping from module name to Nodes. - - """ - - def addNode(self, name, deps): - max_depth, father = 0, None - for n in [Node(x, self) for x in deps]: - if n.depth >= max_depth: - father = n - max_depth = n.depth - if father: - father.addChild(name) - else: - Node(name, self) - - def update_from_db(self, cr): - if not len(self): - return - # update the graph with values from the database (if exist) - ## First, we set the default values for each package in graph - additional_data = dict.fromkeys(self.keys(), {'id': 0, 'state': 'uninstalled', 'dbdemo': False, 'installed_version': None}) - ## Then we get the values from the database - cr.execute('SELECT name, id, state, demo AS dbdemo, latest_version AS installed_version' - ' FROM ir_module_module' - ' WHERE name IN %s',(tuple(additional_data),) - ) - - ## and we update the default values with values from the database - additional_data.update(dict([(x.pop('name'), x) for x in cr.dictfetchall()])) - - for package in self.values(): - for k, v in additional_data[package.name].items(): - setattr(package, k, v) - - def __iter__(self): - level = 0 - done = set(self.keys()) - while done: - level_modules = [(name, module) for name, module in self.items() if module.depth==level] - for name, module in level_modules: - done.remove(name) - yield module - level += 1 - -class Singleton(object): - def __new__(cls, name, graph): - if name in graph: - inst = graph[name] - else: - inst = object.__new__(cls) - inst.name = name - graph[name] = inst - return inst - - -class Node(Singleton): - """ One module in the modules dependency graph. - - Node acts as a per-module singleton. - - """ - - def __init__(self, name, graph): - self.graph = graph - if not hasattr(self, 'children'): - self.children = [] - if not hasattr(self, 'depth'): - self.depth = 0 - - def addChild(self, name): - node = Node(name, self.graph) - node.depth = self.depth + 1 - if node not in self.children: - self.children.append(node) - for attr in ('init', 'update', 'demo'): - if hasattr(self, attr): - setattr(node, attr, True) - self.children.sort(lambda x, y: cmp(x.name, y.name)) - - def __setattr__(self, name, value): - super(Singleton, self).__setattr__(name, value) - if name in ('init', 'update', 'demo'): - tools.config[name][self.name] = 1 - for child in self.children: - setattr(child, name, value) - if name == 'depth': - for child in self.children: - setattr(child, name, value + 1) - - def __iter__(self): - return itertools.chain(iter(self.children), *map(iter, self.children)) - - def __str__(self): - return self._pprint() - - def _pprint(self, depth=0): - s = '%s\n' % self.name - for c in self.children: - s += '%s`-> %s' % (' ' * depth, c._pprint(depth+1)) - return s - def get_module_path(module, downloaded=False): """Return the path of the given module. @@ -407,62 +304,6 @@ def get_modules_with_version(): continue return res -def create_graph(cr, module_list, force=None): - graph = Graph() - upgrade_graph(graph, cr, module_list, force) - return graph - -def upgrade_graph(graph, cr, module_list, force=None): - if force is None: - force = [] - packages = [] - len_graph = len(graph) - for module in module_list: - # This will raise an exception if no/unreadable descriptor file. - # NOTE The call to load_information_from_description_file is already - # done by db.initialize, so it is possible to not do it again here. - info = load_information_from_description_file(module) - if info['installable']: - packages.append((module, info)) # TODO directly a dict, like in get_modules_with_version - else: - logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not installable, skipped' % (module)) - - dependencies = dict([(p, info['depends']) for p, info in packages]) - current, later = set([p for p, info in packages]), set() - - while packages and current > later: - package, info = packages[0] - deps = info['depends'] - - # if all dependencies of 'package' are already in the graph, add 'package' in the graph - if reduce(lambda x, y: x and y in graph, deps, True): - if not package in current: - packages.pop(0) - continue - later.clear() - current.remove(package) - graph.addNode(package, deps) - node = Node(package, graph) - node.data = info - for kind in ('init', 'demo', 'update'): - if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force: - setattr(node, kind, True) - else: - later.add(package) - packages.append((package, info)) - packages.pop(0) - - graph.update_from_db(cr) - - for package in later: - unmet_deps = filter(lambda p: p not in graph, dependencies[package]) - logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: Unmet dependencies: %s' % (package, ', '.join(unmet_deps))) - - result = len(graph) - len_graph - if result != len(module_list): - logger.notifyChannel('init', netsvc.LOG_WARNING, 'Not all modules have loaded.') - return result - def init_module_models(cr, module_name, obj_list): """ Initialize a list of models. @@ -898,7 +739,8 @@ def load_modules(db, force_demo=False, status=None, update_module=False): cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed')) # STEP 1: LOAD BASE (must be done before module dependencies can be computed for later steps) - graph = create_graph(cr, ['base'], force) + graph = openerp.modules.graph.Graph() + graph.add_module(cr, 'base', force) if not graph: logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)') raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)')) @@ -946,7 +788,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False): if not module_list: break - new_modules_in_graph = upgrade_graph(graph, cr, module_list, force) + new_modules_in_graph = graph.add_modules(cr, module_list, force) if new_modules_in_graph == 0: # nothing to load break diff --git a/openerp/modules/graph.py b/openerp/modules/graph.py new file mode 100644 index 00000000000..2a9951f29d9 --- /dev/null +++ b/openerp/modules/graph.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# Copyright (C) 2010-2011 OpenERP s.a. (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import os, sys, imp +from os.path import join as opj +import itertools +import zipimport + +import openerp + +import openerp.osv as osv +import openerp.tools as tools +import openerp.tools.osutil as osutil +from openerp.tools.safe_eval import safe_eval as eval +import openerp.pooler as pooler +from openerp.tools.translate import _ + +import openerp.netsvc as netsvc + +import zipfile +import openerp.release as release + +import re +import base64 +from zipfile import PyZipFile, ZIP_DEFLATED +from cStringIO import StringIO + +import logging + +import openerp.modules.db + +logger = netsvc.Logger() + + +class Graph(dict): + """ Modules dependency graph. + + The graph is a mapping from module name to Nodes. + + """ + + def add_node(self, name, deps): + max_depth, father = 0, None + for n in [Node(x, self) for x in deps]: + if n.depth >= max_depth: + father = n + max_depth = n.depth + if father: + return father.add_child(name) + else: + return Node(name, self) + + def update_from_db(self, cr): + if not len(self): + return + # update the graph with values from the database (if exist) + ## First, we set the default values for each package in graph + additional_data = dict.fromkeys(self.keys(), {'id': 0, 'state': 'uninstalled', 'dbdemo': False, 'installed_version': None}) + ## Then we get the values from the database + cr.execute('SELECT name, id, state, demo AS dbdemo, latest_version AS installed_version' + ' FROM ir_module_module' + ' WHERE name IN %s',(tuple(additional_data),) + ) + + ## and we update the default values with values from the database + additional_data.update(dict([(x.pop('name'), x) for x in cr.dictfetchall()])) + + for package in self.values(): + for k, v in additional_data[package.name].items(): + setattr(package, k, v) + + def add_module(self, cr, module, force=None): + self.add_modules(cr, [module], force) + + def add_modules(self, cr, module_list, force=None): + if force is None: + force = [] + packages = [] + len_graph = len(self) + for module in module_list: + # This will raise an exception if no/unreadable descriptor file. + # NOTE The call to load_information_from_description_file is already + # done by db.initialize, so it is possible to not do it again here. + info = openerp.modules.load_information_from_description_file(module) + if info['installable']: + packages.append((module, info)) # TODO directly a dict, like in get_modules_with_version + else: + logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not installable, skipped' % (module)) + + dependencies = dict([(p, info['depends']) for p, info in packages]) + current, later = set([p for p, info in packages]), set() + + while packages and current > later: + package, info = packages[0] + deps = info['depends'] + + # if all dependencies of 'package' are already in the graph, add 'package' in the graph + if reduce(lambda x, y: x and y in self, deps, True): + if not package in current: + packages.pop(0) + continue + later.clear() + current.remove(package) + node = self.add_node(package, deps) + node.data = info + for kind in ('init', 'demo', 'update'): + if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force: + setattr(node, kind, True) + else: + later.add(package) + packages.append((package, info)) + packages.pop(0) + + self.update_from_db(cr) + + for package in later: + unmet_deps = filter(lambda p: p not in self, dependencies[package]) + logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: Unmet dependencies: %s' % (package, ', '.join(unmet_deps))) + + result = len(self) - len_graph + if result != len(module_list): + logger.notifyChannel('init', netsvc.LOG_WARNING, 'Not all modules have loaded.') + return result + + + def __iter__(self): + level = 0 + done = set(self.keys()) + while done: + level_modules = [(name, module) for name, module in self.items() if module.depth==level] + for name, module in level_modules: + done.remove(name) + yield module + level += 1 + + +class Singleton(object): + def __new__(cls, name, graph): + if name in graph: + inst = graph[name] + else: + inst = object.__new__(cls) + inst.name = name + graph[name] = inst + return inst + + +class Node(Singleton): + """ One module in the modules dependency graph. + + Node acts as a per-module singleton. + + """ + + def __init__(self, name, graph): + self.graph = graph + if not hasattr(self, 'children'): + self.children = [] + if not hasattr(self, 'depth'): + self.depth = 0 + + def add_child(self, name): + node = Node(name, self.graph) + node.depth = self.depth + 1 + if node not in self.children: + self.children.append(node) + for attr in ('init', 'update', 'demo'): + if hasattr(self, attr): + setattr(node, attr, True) + self.children.sort(lambda x, y: cmp(x.name, y.name)) + return node + + def __setattr__(self, name, value): + super(Singleton, self).__setattr__(name, value) + if name in ('init', 'update', 'demo'): + tools.config[name][self.name] = 1 + for child in self.children: + setattr(child, name, value) + if name == 'depth': + for child in self.children: + setattr(child, name, value + 1) + + def __iter__(self): + return itertools.chain(iter(self.children), *map(iter, self.children)) + + def __str__(self): + return self._pprint() + + def _pprint(self, depth=0): + s = '%s\n' % self.name + for c in self.children: + s += '%s`-> %s' % (' ' * depth, c._pprint(depth+1)) + return s + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From fb476648e0dce64cfbef0d10bee29e0ea6127a16 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 11 May 2011 18:41:44 +0200 Subject: [PATCH 29/35] [REF] openerp.modules: migration manager code moved to openerp.modules.migration. bzr revid: vmt@openerp.com-20110511164144-8o1bl6jfb8k7keak --- openerp/modules/__init__.py | 149 +------------------------ openerp/modules/graph.py | 2 + openerp/modules/migration.py | 204 +++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 147 deletions(-) create mode 100644 openerp/modules/migration.py diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py index 539abc6553f..cf8d022842e 100644 --- a/openerp/modules/__init__.py +++ b/openerp/modules/__init__.py @@ -52,6 +52,7 @@ import logging import openerp.modules.db import openerp.modules.graph +import openerp.modules.migration logger = netsvc.Logger() @@ -385,152 +386,6 @@ def register_module_classes(m): loaded.append(m) -class MigrationManager(object): - """ - This class manage the migration of modules - Migrations files must be python files containing a "migrate(cr, installed_version)" function. - Theses files must respect a directory tree structure: A 'migrations' folder which containt a - folder by version. Version can be 'module' version or 'server.module' version (in this case, - the files will only be processed by this version of the server). Python file names must start - by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation - Example: - - - `-- migrations - |-- 1.0 - | |-- pre-update_table_x.py - | |-- pre-update_table_y.py - | |-- post-clean-data.py - | `-- README.txt # not processed - |-- 5.0.1.1 # files in this folder will be executed only on a 5.0 server - | |-- pre-delete_table_z.py - | `-- post-clean-data.py - `-- foo.py # not processed - - This similar structure is generated by the maintenance module with the migrations files get by - the maintenance contract - - """ - def __init__(self, cr, graph): - self.cr = cr - self.graph = graph - self.migrations = {} - self._get_files() - - def _get_files(self): - - """ - import addons.base.maintenance.utils as maintenance_utils - maintenance_utils.update_migrations_files(self.cr) - #""" - - for pkg in self.graph: - self.migrations[pkg.name] = {} - if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'): - continue - - self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {} - self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {} - - def migrate_module(self, pkg, stage): - assert stage in ('pre', 'post') - stageformat = {'pre': '[>%s]', - 'post': '[%s>]', - } - - if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'): - return - - def convert_version(version): - if version.startswith(release.major_version) and version != release.major_version: - return version # the version number already containt the server version - return "%s.%s" % (release.major_version, version) - - def _get_migration_versions(pkg): - def __get_dir(tree): - return [d for d in tree if tree[d] is not None] - - versions = list(set( - __get_dir(self.migrations[pkg.name]['module']) + - __get_dir(self.migrations[pkg.name]['maintenance']) - )) - versions.sort(key=lambda k: parse_version(convert_version(k))) - return versions - - def _get_migration_files(pkg, version, stage): - """ return a list of tuple (module, file) - """ - m = self.migrations[pkg.name] - lst = [] - - mapping = {'module': opj(pkg.name, 'migrations'), - 'maintenance': opj('base', 'maintenance', 'migrations', pkg.name), - } - - for x in mapping.keys(): - if version in m[x]: - for f in m[x][version]: - if m[x][version][f] is not None: - continue - if not f.startswith(stage + '-'): - continue - lst.append(opj(mapping[x], version, f)) - lst.sort() - return lst - - def mergedict(a, b): - a = a.copy() - a.update(b) - return a - - from openerp.tools.parse_version import parse_version - - parsed_installed_version = parse_version(pkg.installed_version or '') - current_version = parse_version(convert_version(pkg.data['version'])) - - versions = _get_migration_versions(pkg) - - for version in versions: - if parsed_installed_version < parse_version(convert_version(version)) <= current_version: - - strfmt = {'addon': pkg.name, - 'stage': stage, - 'version': stageformat[stage] % version, - } - - for pyfile in _get_migration_files(pkg, version, stage): - name, ext = os.path.splitext(os.path.basename(pyfile)) - if ext.lower() != '.py': - continue - mod = fp = fp2 = None - try: - fp = tools.file_open(pyfile) - - # imp.load_source need a real file object, so we create - # one from the file-like object we get from file_open - fp2 = os.tmpfile() - fp2.write(fp.read()) - fp2.seek(0) - try: - mod = imp.load_source(name, pyfile, fp2) - logger.notifyChannel('migration', netsvc.LOG_INFO, 'module %(addon)s: Running migration %(version)s %(name)s' % mergedict({'name': mod.__name__}, strfmt)) - mod.migrate(self.cr, pkg.installed_version) - except ImportError: - logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % mergedict({'file': pyfile}, strfmt)) - raise - except AttributeError: - logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt) - except: - raise - finally: - if fp: - fp.close() - if fp2: - fp2.close() - if mod: - del mod - - def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, report=None): """Migrates+Updates or Installs all module nodes from ``graph`` :param graph: graph of module nodes to load @@ -613,7 +468,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= processed_modules = [] statusi = 0 pool = pooler.get_pool(cr.dbname) - migrations = MigrationManager(cr, graph) + migrations = openerp.modules.migration.MigrationManager(cr, graph) logger.notifyChannel('init', netsvc.LOG_DEBUG, 'loading %d packages..' % len(graph)) # register, instanciate and initialize models for each modules diff --git a/openerp/modules/graph.py b/openerp/modules/graph.py index 2a9951f29d9..e6938805266 100644 --- a/openerp/modules/graph.py +++ b/openerp/modules/graph.py @@ -20,6 +20,8 @@ # ############################################################################## +""" Modules dependency graph. """ + import os, sys, imp from os.path import join as opj import itertools diff --git a/openerp/modules/migration.py b/openerp/modules/migration.py new file mode 100644 index 00000000000..0773a192ffb --- /dev/null +++ b/openerp/modules/migration.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# Copyright (C) 2010-2011 OpenERP s.a. (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +""" Modules migration handling. """ + +import os, sys, imp +from os.path import join as opj +import itertools +import zipimport + +import openerp + +import openerp.osv as osv +import openerp.tools as tools +import openerp.tools.osutil as osutil +from openerp.tools.safe_eval import safe_eval as eval +import openerp.pooler as pooler +from openerp.tools.translate import _ + +import openerp.netsvc as netsvc + +import zipfile +import openerp.release as release + +import re +import base64 +from zipfile import PyZipFile, ZIP_DEFLATED +from cStringIO import StringIO + +import logging + +import openerp.modules.db +import openerp.modules.graph + +logger = netsvc.Logger() + + +class MigrationManager(object): + """ + This class manage the migration of modules + Migrations files must be python files containing a "migrate(cr, installed_version)" function. + Theses files must respect a directory tree structure: A 'migrations' folder which containt a + folder by version. Version can be 'module' version or 'server.module' version (in this case, + the files will only be processed by this version of the server). Python file names must start + by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation + Example: + + + `-- migrations + |-- 1.0 + | |-- pre-update_table_x.py + | |-- pre-update_table_y.py + | |-- post-clean-data.py + | `-- README.txt # not processed + |-- 5.0.1.1 # files in this folder will be executed only on a 5.0 server + | |-- pre-delete_table_z.py + | `-- post-clean-data.py + `-- foo.py # not processed + + This similar structure is generated by the maintenance module with the migrations files get by + the maintenance contract + + """ + def __init__(self, cr, graph): + self.cr = cr + self.graph = graph + self.migrations = {} + self._get_files() + + def _get_files(self): + + """ + import addons.base.maintenance.utils as maintenance_utils + maintenance_utils.update_migrations_files(self.cr) + #""" + + for pkg in self.graph: + self.migrations[pkg.name] = {} + if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'): + continue + + get_module_filetree = openerp.modules.get_module_filetree + self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {} + self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {} + + def migrate_module(self, pkg, stage): + assert stage in ('pre', 'post') + stageformat = {'pre': '[>%s]', + 'post': '[%s>]', + } + + if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'): + return + + def convert_version(version): + if version.startswith(release.major_version) and version != release.major_version: + return version # the version number already containt the server version + return "%s.%s" % (release.major_version, version) + + def _get_migration_versions(pkg): + def __get_dir(tree): + return [d for d in tree if tree[d] is not None] + + versions = list(set( + __get_dir(self.migrations[pkg.name]['module']) + + __get_dir(self.migrations[pkg.name]['maintenance']) + )) + versions.sort(key=lambda k: parse_version(convert_version(k))) + return versions + + def _get_migration_files(pkg, version, stage): + """ return a list of tuple (module, file) + """ + m = self.migrations[pkg.name] + lst = [] + + mapping = {'module': opj(pkg.name, 'migrations'), + 'maintenance': opj('base', 'maintenance', 'migrations', pkg.name), + } + + for x in mapping.keys(): + if version in m[x]: + for f in m[x][version]: + if m[x][version][f] is not None: + continue + if not f.startswith(stage + '-'): + continue + lst.append(opj(mapping[x], version, f)) + lst.sort() + return lst + + def mergedict(a, b): + a = a.copy() + a.update(b) + return a + + from openerp.tools.parse_version import parse_version + + parsed_installed_version = parse_version(pkg.installed_version or '') + current_version = parse_version(convert_version(pkg.data['version'])) + + versions = _get_migration_versions(pkg) + + for version in versions: + if parsed_installed_version < parse_version(convert_version(version)) <= current_version: + + strfmt = {'addon': pkg.name, + 'stage': stage, + 'version': stageformat[stage] % version, + } + + for pyfile in _get_migration_files(pkg, version, stage): + name, ext = os.path.splitext(os.path.basename(pyfile)) + if ext.lower() != '.py': + continue + mod = fp = fp2 = None + try: + fp = tools.file_open(pyfile) + + # imp.load_source need a real file object, so we create + # one from the file-like object we get from file_open + fp2 = os.tmpfile() + fp2.write(fp.read()) + fp2.seek(0) + try: + mod = imp.load_source(name, pyfile, fp2) + logger.notifyChannel('migration', netsvc.LOG_INFO, 'module %(addon)s: Running migration %(version)s %(name)s' % mergedict({'name': mod.__name__}, strfmt)) + mod.migrate(self.cr, pkg.installed_version) + except ImportError: + logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % mergedict({'file': pyfile}, strfmt)) + raise + except AttributeError: + logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt) + except: + raise + finally: + if fp: + fp.close() + if fp2: + fp2.close() + if mod: + del mod + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 1fe579d8ec2a246dbfc2e4fa8cbd7df5497929bf Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 11 May 2011 19:24:48 +0200 Subject: [PATCH 30/35] [REF] module code goes into openerp.modules.module. bzr revid: vmt@openerp.com-20110511172448-p12p1rizvf3lqv97 --- openerp/modules/__init__.py | 328 +----------------------------- openerp/modules/graph.py | 4 +- openerp/modules/migration.py | 2 +- openerp/modules/module.py | 379 +++++++++++++++++++++++++++++++++++ openerp/tools/misc.py | 2 +- 5 files changed, 389 insertions(+), 326 deletions(-) create mode 100644 openerp/modules/module.py diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py index cf8d022842e..c37016628a9 100644 --- a/openerp/modules/__init__.py +++ b/openerp/modules/__init__.py @@ -54,31 +54,15 @@ import openerp.modules.db import openerp.modules.graph import openerp.modules.migration +from openerp.modules.module import \ + get_modules, get_modules_with_version, \ + load_information_from_description_file, \ + get_module_resource, zip_directory, \ + get_module_path, initialize_sys_path, \ + register_module_classes, init_module_models + logger = netsvc.Logger() -_ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base) -ad_paths = [] - -# Modules already loaded -loaded = [] - -def initialize_sys_path(): - global ad_paths - - if ad_paths: - return - - ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(',')) - - sys.path.insert(1, _ad) - - ad_cnt=1 - for adp in ad_paths: - if adp != _ad: - sys.path.insert(ad_cnt, adp) - ad_cnt+=1 - - ad_paths.append(_ad) # for get_module_path def open_openerp_namespace(): # See comment for open_openerp_namespace. @@ -88,304 +72,6 @@ def open_openerp_namespace(): sys.modules[k[8:]] = v -def get_module_path(module, downloaded=False): - """Return the path of the given module. - - Search the addons paths and return the first path where the given - module is found. If downloaded is True, return the default addons - path if nothing else is found. - - """ - initialize_sys_path() - for adp in ad_paths: - if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)): - return opj(adp, module) - - if downloaded: - return opj(_ad, module) - logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,)) - return False - - -def get_module_filetree(module, dir='.'): - path = get_module_path(module) - if not path: - return False - - dir = os.path.normpath(dir) - if dir == '.': - dir = '' - if dir.startswith('..') or (dir and dir[0] == '/'): - raise Exception('Cannot access file outside the module') - - if not os.path.isdir(path): - # zipmodule - zip = zipfile.ZipFile(path + ".zip") - files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()] - else: - files = osutil.listdir(path, True) - - tree = {} - for f in files: - if not f.startswith(dir): - continue - - if dir: - f = f[len(dir)+int(not dir.endswith('/')):] - lst = f.split(os.sep) - current = tree - while len(lst) != 1: - current = current.setdefault(lst.pop(0), {}) - current[lst.pop(0)] = None - - return tree - -def zip_directory(directory, b64enc=True, src=True): - """Compress a directory - - @param directory: The directory to compress - @param base64enc: if True the function will encode the zip file with base64 - @param src: Integrate the source files - - @return: a string containing the zip file - """ - - RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I) - - def _zippy(archive, path, src=True): - path = os.path.abspath(path) - base = os.path.basename(path) - for f in osutil.listdir(path, True): - bf = os.path.basename(f) - if not RE_exclude.search(bf) and (src or bf in ('__openerp__.py', '__terp__.py') or not bf.endswith('.py')): - archive.write(os.path.join(path, f), os.path.join(base, f)) - - archname = StringIO() - archive = PyZipFile(archname, "w", ZIP_DEFLATED) - - # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8) - directory = tools.ustr(directory).encode('utf-8') - - archive.writepy(directory) - _zippy(archive, directory, src=src) - archive.close() - archive_data = archname.getvalue() - archname.close() - - if b64enc: - return base64.encodestring(archive_data) - - return archive_data - -def get_module_as_zip(modulename, b64enc=True, src=True): - """Generate a module as zip file with the source or not and can do a base64 encoding - - @param modulename: The module name - @param b64enc: if True the function will encode the zip file with base64 - @param src: Integrate the source files - - @return: a stream to store in a file-like object - """ - - ap = get_module_path(str(modulename)) - if not ap: - raise Exception('Unable to find path for module %s' % modulename) - - ap = ap.encode('utf8') - if os.path.isfile(ap + '.zip'): - val = file(ap + '.zip', 'rb').read() - if b64enc: - val = base64.encodestring(val) - else: - val = zip_directory(ap, b64enc, src) - - return val - - -def get_module_resource(module, *args): - """Return the full path of a resource of the given module. - - @param module: the module - @param args: the resource path components - - @return: absolute path to the resource - - TODO name it get_resource_path - TODO make it available inside on osv object (self.get_resource_path) - """ - a = get_module_path(module) - if not a: return False - resource_path = opj(a, *args) - if zipfile.is_zipfile( a +'.zip') : - zip = zipfile.ZipFile( a + ".zip") - files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()] - resource_path = '/'.join(args) - if resource_path in files: - return opj(a, resource_path) - elif os.path.exists(resource_path): - return resource_path - return False - -def get_modules(): - """Returns the list of module names - """ - def listdir(dir): - def clean(name): - name = os.path.basename(name) - if name[-4:] == '.zip': - name = name[:-4] - return name - - def is_really_module(name): - name = opj(dir, name) - return os.path.isdir(name) or zipfile.is_zipfile(name) - return map(clean, filter(is_really_module, os.listdir(dir))) - - plist = [] - initialize_sys_path() - for ad in ad_paths: - plist.extend(listdir(ad)) - return list(set(plist)) - -def load_information_from_description_file(module): - """ - :param module: The name of the module (sale, purchase, ...) - """ - - terp_file = get_module_resource(module, '__openerp__.py') - if not terp_file: - terp_file = get_module_resource(module, '__terp__.py') - mod_path = get_module_path(module) - if terp_file: - info = {} - if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'): - terp_f = tools.file_open(terp_file) - try: - info = eval(terp_f.read()) - except Exception: - logger.notifyChannel('modules', netsvc.LOG_ERROR, - 'module %s: exception while evaluating file %s' % - (module, terp_file)) - raise - finally: - terp_f.close() - # TODO the version should probably be mandatory - info.setdefault('version', '0') - info.setdefault('category', 'Uncategorized') - info.setdefault('depends', []) - info.setdefault('author', '') - info.setdefault('website', '') - info.setdefault('name', False) - info.setdefault('description', '') - info['certificate'] = info.get('certificate') or None - info['web'] = info.get('web') or False - info['license'] = info.get('license') or 'AGPL-3' - info.setdefault('installable', True) - info.setdefault('active', False) - for kind in ['data', 'demo', 'test', - 'init_xml', 'update_xml', 'demo_xml']: - info.setdefault(kind, []) - return info - - #TODO: refactor the logger in this file to follow the logging guidelines - # for 6.0 - logging.getLogger('modules').debug('module %s: no descriptor file' - ' found: __openerp__.py or __terp__.py (deprecated)', module) - return {} - - -def get_modules_with_version(): - modules = get_modules() - res = {} - for module in modules: - try: - info = load_information_from_description_file(module) - res[module] = "%s.%s" % (release.major_version, info['version']) - except Exception, e: - continue - return res - - -def init_module_models(cr, module_name, obj_list): - """ Initialize a list of models. - - Call _auto_init and init on each model to create or update the - database tables supporting the models. - - TODO better explanation of _auto_init and init. - - """ - - logger.notifyChannel('init', netsvc.LOG_INFO, - 'module %s: creating or updating database tables' % module_name) - # TODO _auto_init doesn't seem to return anything - # so this todo list would be useless. - todo = [] - for obj in obj_list: - try: - # TODO the module in the context doesn't seem usefull: - # it is available (at least) in the class' _module attribute. - # (So module_name would be useless too.) - result = obj._auto_init(cr, {'module': module_name}) - except Exception, e: - raise - if result: - todo += result - if hasattr(obj, 'init'): - obj.init(cr) - cr.commit() - todo.sort() - for t in todo: - t[1](cr, *t[2]) - cr.commit() - - -def load_module(module_name): - """ Load a Python module found on the addons paths.""" - fm = imp.find_module(module_name, ad_paths) - try: - imp.load_module(module_name, *fm) - finally: - if fm[0]: - fm[0].close() - - -def register_module_classes(m): - """ Register module named m, if not already registered. - - This will load the module and register all of its models. (Actually, the - explicit constructor call of each of the models inside the module will - register them.) - - """ - - def log(e): - mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or '' - msg = "Couldn't load %smodule %s" % (mt, m) - logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg) - logger.notifyChannel('init', netsvc.LOG_CRITICAL, e) - - global loaded - if m in loaded: - return - logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m) - mod_path = get_module_path(m) - - initialize_sys_path() - try: - zip_mod_path = mod_path + '.zip' - if not os.path.isfile(zip_mod_path): - load_module(m) - else: - zimp = zipimport.zipimporter(zip_mod_path) - zimp.load_module(m) - except Exception, e: - log(e) - raise - else: - loaded.append(m) - - def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, report=None): """Migrates+Updates or Installs all module nodes from ``graph`` :param graph: graph of module nodes to load diff --git a/openerp/modules/graph.py b/openerp/modules/graph.py index e6938805266..078d23c235b 100644 --- a/openerp/modules/graph.py +++ b/openerp/modules/graph.py @@ -48,8 +48,6 @@ from cStringIO import StringIO import logging -import openerp.modules.db - logger = netsvc.Logger() @@ -102,7 +100,7 @@ class Graph(dict): # This will raise an exception if no/unreadable descriptor file. # NOTE The call to load_information_from_description_file is already # done by db.initialize, so it is possible to not do it again here. - info = openerp.modules.load_information_from_description_file(module) + info = openerp.modules.module.load_information_from_description_file(module) if info['installable']: packages.append((module, info)) # TODO directly a dict, like in get_modules_with_version else: diff --git a/openerp/modules/migration.py b/openerp/modules/migration.py index 0773a192ffb..d26f5927141 100644 --- a/openerp/modules/migration.py +++ b/openerp/modules/migration.py @@ -98,7 +98,7 @@ class MigrationManager(object): if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'): continue - get_module_filetree = openerp.modules.get_module_filetree + get_module_filetree = openerp.modules.module.get_module_filetree self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {} self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {} diff --git a/openerp/modules/module.py b/openerp/modules/module.py new file mode 100644 index 00000000000..bf53ca6d87c --- /dev/null +++ b/openerp/modules/module.py @@ -0,0 +1,379 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# Copyright (C) 2010-2011 OpenERP s.a. (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import os, sys, imp +from os.path import join as opj +import itertools +import zipimport + +import openerp + +import openerp.osv as osv +import openerp.tools as tools +import openerp.tools.osutil as osutil +from openerp.tools.safe_eval import safe_eval as eval +import openerp.pooler as pooler +from openerp.tools.translate import _ + +import openerp.netsvc as netsvc + +import zipfile +import openerp.release as release + +import re +import base64 +from zipfile import PyZipFile, ZIP_DEFLATED +from cStringIO import StringIO + +import logging + +import openerp.modules.db +import openerp.modules.graph + +_ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base) +ad_paths = [] + +# Modules already loaded +loaded = [] + +logger = netsvc.Logger() + +def initialize_sys_path(): + global ad_paths + + if ad_paths: + return + + ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(',')) + + sys.path.insert(1, _ad) + + ad_cnt=1 + for adp in ad_paths: + if adp != _ad: + sys.path.insert(ad_cnt, adp) + ad_cnt+=1 + + ad_paths.append(_ad) # for get_module_path + + +def get_module_path(module, downloaded=False): + """Return the path of the given module. + + Search the addons paths and return the first path where the given + module is found. If downloaded is True, return the default addons + path if nothing else is found. + + """ + initialize_sys_path() + for adp in ad_paths: + if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)): + return opj(adp, module) + + if downloaded: + return opj(_ad, module) + logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,)) + return False + + +def get_module_filetree(module, dir='.'): + path = get_module_path(module) + if not path: + return False + + dir = os.path.normpath(dir) + if dir == '.': + dir = '' + if dir.startswith('..') or (dir and dir[0] == '/'): + raise Exception('Cannot access file outside the module') + + if not os.path.isdir(path): + # zipmodule + zip = zipfile.ZipFile(path + ".zip") + files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()] + else: + files = osutil.listdir(path, True) + + tree = {} + for f in files: + if not f.startswith(dir): + continue + + if dir: + f = f[len(dir)+int(not dir.endswith('/')):] + lst = f.split(os.sep) + current = tree + while len(lst) != 1: + current = current.setdefault(lst.pop(0), {}) + current[lst.pop(0)] = None + + return tree + +def zip_directory(directory, b64enc=True, src=True): + """Compress a directory + + @param directory: The directory to compress + @param base64enc: if True the function will encode the zip file with base64 + @param src: Integrate the source files + + @return: a string containing the zip file + """ + + RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I) + + def _zippy(archive, path, src=True): + path = os.path.abspath(path) + base = os.path.basename(path) + for f in osutil.listdir(path, True): + bf = os.path.basename(f) + if not RE_exclude.search(bf) and (src or bf in ('__openerp__.py', '__terp__.py') or not bf.endswith('.py')): + archive.write(os.path.join(path, f), os.path.join(base, f)) + + archname = StringIO() + archive = PyZipFile(archname, "w", ZIP_DEFLATED) + + # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8) + directory = tools.ustr(directory).encode('utf-8') + + archive.writepy(directory) + _zippy(archive, directory, src=src) + archive.close() + archive_data = archname.getvalue() + archname.close() + + if b64enc: + return base64.encodestring(archive_data) + + return archive_data + +def get_module_as_zip(modulename, b64enc=True, src=True): + """Generate a module as zip file with the source or not and can do a base64 encoding + + @param modulename: The module name + @param b64enc: if True the function will encode the zip file with base64 + @param src: Integrate the source files + + @return: a stream to store in a file-like object + """ + + ap = get_module_path(str(modulename)) + if not ap: + raise Exception('Unable to find path for module %s' % modulename) + + ap = ap.encode('utf8') + if os.path.isfile(ap + '.zip'): + val = file(ap + '.zip', 'rb').read() + if b64enc: + val = base64.encodestring(val) + else: + val = zip_directory(ap, b64enc, src) + + return val + + +def get_module_resource(module, *args): + """Return the full path of a resource of the given module. + + @param module: the module + @param args: the resource path components + + @return: absolute path to the resource + + TODO name it get_resource_path + TODO make it available inside on osv object (self.get_resource_path) + """ + a = get_module_path(module) + if not a: return False + resource_path = opj(a, *args) + if zipfile.is_zipfile( a +'.zip') : + zip = zipfile.ZipFile( a + ".zip") + files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()] + resource_path = '/'.join(args) + if resource_path in files: + return opj(a, resource_path) + elif os.path.exists(resource_path): + return resource_path + return False + + +def load_information_from_description_file(module): + """ + :param module: The name of the module (sale, purchase, ...) + """ + + terp_file = get_module_resource(module, '__openerp__.py') + if not terp_file: + terp_file = get_module_resource(module, '__terp__.py') + mod_path = get_module_path(module) + if terp_file: + info = {} + if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'): + terp_f = tools.file_open(terp_file) + try: + info = eval(terp_f.read()) + except Exception: + logger.notifyChannel('modules', netsvc.LOG_ERROR, + 'module %s: exception while evaluating file %s' % + (module, terp_file)) + raise + finally: + terp_f.close() + # TODO the version should probably be mandatory + info.setdefault('version', '0') + info.setdefault('category', 'Uncategorized') + info.setdefault('depends', []) + info.setdefault('author', '') + info.setdefault('website', '') + info.setdefault('name', False) + info.setdefault('description', '') + info['certificate'] = info.get('certificate') or None + info['web'] = info.get('web') or False + info['license'] = info.get('license') or 'AGPL-3' + info.setdefault('installable', True) + info.setdefault('active', False) + for kind in ['data', 'demo', 'test', + 'init_xml', 'update_xml', 'demo_xml']: + info.setdefault(kind, []) + return info + + #TODO: refactor the logger in this file to follow the logging guidelines + # for 6.0 + logging.getLogger('modules').debug('module %s: no descriptor file' + ' found: __openerp__.py or __terp__.py (deprecated)', module) + return {} + + +def init_module_models(cr, module_name, obj_list): + """ Initialize a list of models. + + Call _auto_init and init on each model to create or update the + database tables supporting the models. + + TODO better explanation of _auto_init and init. + + """ + + logger.notifyChannel('init', netsvc.LOG_INFO, + 'module %s: creating or updating database tables' % module_name) + # TODO _auto_init doesn't seem to return anything + # so this todo list would be useless. + todo = [] + for obj in obj_list: + try: + # TODO the module in the context doesn't seem usefull: + # it is available (at least) in the class' _module attribute. + # (So module_name would be useless too.) + result = obj._auto_init(cr, {'module': module_name}) + except Exception, e: + raise + if result: + todo += result + if hasattr(obj, 'init'): + obj.init(cr) + cr.commit() + todo.sort() + for t in todo: + t[1](cr, *t[2]) + cr.commit() + + +def load_module(module_name): + """ Load a Python module found on the addons paths.""" + fm = imp.find_module(module_name, ad_paths) + try: + imp.load_module(module_name, *fm) + finally: + if fm[0]: + fm[0].close() + + +def register_module_classes(m): + """ Register module named m, if not already registered. + + This will load the module and register all of its models. (Actually, the + explicit constructor call of each of the models inside the module will + register them.) + + """ + + def log(e): + mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or '' + msg = "Couldn't load %smodule %s" % (mt, m) + logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg) + logger.notifyChannel('init', netsvc.LOG_CRITICAL, e) + + global loaded + if m in loaded: + return + logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m) + mod_path = get_module_path(m) + + initialize_sys_path() + try: + zip_mod_path = mod_path + '.zip' + if not os.path.isfile(zip_mod_path): + load_module(m) + else: + zimp = zipimport.zipimporter(zip_mod_path) + zimp.load_module(m) + except Exception, e: + log(e) + raise + else: + loaded.append(m) + + +def get_modules(): + """Returns the list of module names + """ + def listdir(dir): + def clean(name): + name = os.path.basename(name) + if name[-4:] == '.zip': + name = name[:-4] + return name + + def is_really_module(name): + name = opj(dir, name) + return os.path.isdir(name) or zipfile.is_zipfile(name) + return map(clean, filter(is_really_module, os.listdir(dir))) + + plist = [] + initialize_sys_path() + for ad in ad_paths: + plist.extend(listdir(ad)) + return list(set(plist)) + + +def get_modules_with_version(): + modules = get_modules() + res = {} + for module in modules: + try: + info = load_information_from_description_file(module) + res[module] = "%s.%s" % (release.major_version, info['version']) + except Exception, e: + continue + return res + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index f232dd4ceac..5aa01d4f267 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -135,7 +135,7 @@ def file_open(name, mode="r", subdir='addons', pathinfo=False): @return: fileobject if pathinfo is False else (fileobject, filepath) """ import openerp.modules as addons - adps = addons.ad_paths + adps = addons.module.ad_paths rtp = os.path.normcase(os.path.abspath(config['root_path'])) if name.replace(os.path.sep, '/').startswith('addons/'): From 40bce2fead943e85d6318098845e545ed8cd5427 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 11 May 2011 19:48:41 +0200 Subject: [PATCH 31/35] [REF] renamed modules.__init__ to modules.loading. bzr revid: vmt@openerp.com-20110511174841-wk1yyr9971pu0pwh --- openerp/modules/__init__.py | 386 +-------------------------------- openerp/modules/loading.py | 422 ++++++++++++++++++++++++++++++++++++ 2 files changed, 426 insertions(+), 382 deletions(-) create mode 100644 openerp/modules/loading.py diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py index c37016628a9..171d8418269 100644 --- a/openerp/modules/__init__.py +++ b/openerp/modules/__init__.py @@ -24,36 +24,13 @@ """ -import os, sys, imp -from os.path import join as opj -import itertools -import zipimport - -import openerp - -import openerp.osv as osv -import openerp.tools as tools -import openerp.tools.osutil as osutil -from openerp.tools.safe_eval import safe_eval as eval -import openerp.pooler as pooler -from openerp.tools.translate import _ - -import openerp.netsvc as netsvc - -import zipfile -import openerp.release as release - -import re -import base64 -from zipfile import PyZipFile, ZIP_DEFLATED -from cStringIO import StringIO - -import logging - import openerp.modules.db import openerp.modules.graph +import openerp.modules.loading import openerp.modules.migration +import openerp.modules.module +# TODO temporarily expose those things from openerp.modules.module import \ get_modules, get_modules_with_version, \ load_information_from_description_file, \ @@ -61,362 +38,7 @@ from openerp.modules.module import \ get_module_path, initialize_sys_path, \ register_module_classes, init_module_models -logger = netsvc.Logger() - - -def open_openerp_namespace(): - # See comment for open_openerp_namespace. - if openerp.conf.deprecation.open_openerp_namespace: - for k, v in list(sys.modules.items()): - if k.startswith('openerp.') and sys.modules.get(k[8:]) is None: - sys.modules[k[8:]] = v - - -def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, report=None): - """Migrates+Updates or Installs all module nodes from ``graph`` - :param graph: graph of module nodes to load - :param status: status dictionary for keeping track of progress - :param perform_checks: whether module descriptors should be checked for validity (prints warnings - for same cases, and even raise osv_except if certificate is invalid) - :param skip_modules: optional list of module names (packages) which have previously been loaded and can be skipped - :return: list of modules that were installed or updated - """ - def process_sql_file(cr, fp): - queries = fp.read().split(';') - for query in queries: - new_query = ' '.join(query.split()) - if new_query: - cr.execute(new_query) - - def load_init_xml(cr, m, idref, mode): - _load_data(cr, m, idref, mode, 'init_xml') - - def load_update_xml(cr, m, idref, mode): - _load_data(cr, m, idref, mode, 'update_xml') - - def load_demo_xml(cr, m, idref, mode): - _load_data(cr, m, idref, mode, 'demo_xml') - - def load_data(cr, module_name, idref, mode): - _load_data(cr, module_name, idref, mode, 'data') - - def load_demo(cr, module_name, idref, mode): - _load_data(cr, module_name, idref, mode, 'demo') - - def load_test(cr, module_name, idref, mode): - cr.commit() - if not tools.config.options['test_disable']: - try: - _load_data(cr, module_name, idref, mode, 'test') - except Exception, e: - logging.getLogger('test').exception('Tests failed to execute in module %s', module_name) - finally: - if tools.config.options['test_commit']: - cr.commit() - else: - cr.rollback() - - def _load_data(cr, module_name, idref, mode, kind): - """ - - kind: data, demo, test, init_xml, update_xml, demo_xml. - - noupdate is False, unless it is demo data or it is csv data in - init mode. - - """ - for filename in package.data[kind]: - log = logging.getLogger('init') - log.info("module %s: loading %s", module_name, filename) - _, ext = os.path.splitext(filename) - pathname = os.path.join(module_name, filename) - fp = tools.file_open(pathname) - noupdate = False - if kind in ('demo', 'demo_xml'): - noupdate = True - try: - if ext == '.csv': - if kind in ('init', 'init_xml'): - noupdate = True - tools.convert_csv_import(cr, module_name, pathname, fp.read(), idref, mode, noupdate) - elif ext == '.sql': - process_sql_file(cr, fp) - elif ext == '.yml': - tools.convert_yaml_import(cr, module_name, fp, idref, mode, noupdate) - else: - tools.convert_xml_import(cr, module_name, fp, idref, mode, noupdate, report) - finally: - fp.close() - - if status is None: - status = {} - - processed_modules = [] - statusi = 0 - pool = pooler.get_pool(cr.dbname) - migrations = openerp.modules.migration.MigrationManager(cr, graph) - logger.notifyChannel('init', netsvc.LOG_DEBUG, 'loading %d packages..' % len(graph)) - - # register, instanciate and initialize models for each modules - for package in graph: - if skip_modules and package.name in skip_modules: - continue - logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name) - migrations.migrate_module(package, 'pre') - register_module_classes(package.name) - models = pool.instanciate(package.name, cr) - if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'): - init_module_models(cr, package.name, models) - cr.commit() - - # load data for each modules - modobj = pool.get('ir.module.module') - for package in graph: - status['progress'] = (float(statusi)+0.1) / len(graph) - m = package.name - mid = package.id - - if skip_modules and m in skip_modules: - continue - - if perform_checks: - modobj.check(cr, 1, [mid]) - - idref = {} - status['progress'] = (float(statusi)+0.4) / len(graph) - - mode = 'update' - if hasattr(package, 'init') or package.state == 'to install': - mode = 'init' - - if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'): - if package.state=='to upgrade': - # upgrading the module information - modobj.write(cr, 1, [mid], modobj.get_values_from_terp(package.data)) - load_init_xml(cr, m, idref, mode) - load_update_xml(cr, m, idref, mode) - load_data(cr, m, idref, mode) - if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'): - status['progress'] = (float(statusi)+0.75) / len(graph) - load_demo_xml(cr, m, idref, mode) - load_demo(cr, m, idref, mode) - cr.execute('update ir_module_module set demo=%s where id=%s', (True, mid)) - - # launch tests only in demo mode, as most tests will depend - # on demo data. Other tests can be added into the regular - # 'data' section, but should probably not alter the data, - # as there is no rollback. - load_test(cr, m, idref, mode) - - processed_modules.append(package.name) - - migrations.migrate_module(package, 'post') - - ver = release.major_version + '.' + package.data['version'] - # Set new modules and dependencies - modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver}) - cr.commit() - # Update translations for all installed languages - modobj.update_translations(cr, 1, [mid], None) - cr.commit() - - package.state = 'installed' - for kind in ('init', 'demo', 'update'): - if hasattr(package, kind): - delattr(package, kind) - - statusi += 1 - - cr.commit() - - return processed_modules - -def _check_module_names(cr, module_names): - mod_names = set(module_names) - if 'base' in mod_names: - # ignore dummy 'all' module - if 'all' in mod_names: - mod_names.remove('all') - if mod_names: - cr.execute("SELECT count(id) AS count FROM ir_module_module WHERE name in %s", (tuple(mod_names),)) - if cr.dictfetchone()['count'] != len(mod_names): - # find out what module name(s) are incorrect: - cr.execute("SELECT name FROM ir_module_module") - incorrect_names = mod_names.difference([x['name'] for x in cr.dictfetchall()]) - logging.getLogger('init').warning('invalid module names, ignored: %s', ", ".join(incorrect_names)) - -def load_modules(db, force_demo=False, status=None, update_module=False): - # TODO status['progress'] reporting is broken: used twice (and reset each - # time to zero) in load_module_graph, not fine-grained enough. - # It should be a method exposed by the pool. - - initialize_sys_path() - - open_openerp_namespace() - - cr = db.cursor() - if cr: - cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='ir_module_module'") - if len(cr.fetchall())==0: - logger.notifyChannel("init", netsvc.LOG_INFO, "init db") - openerp.modules.db.initialize(cr) - tools.config["init"]["all"] = 1 - tools.config['update']['all'] = 1 - if not tools.config['without_demo']: - tools.config["demo"]['all'] = 1 - force = [] - if force_demo: - force.append('demo') - - # This is a brand new pool, just created in pooler.get_db_and_pool() - pool = pooler.get_pool(cr.dbname) - - try: - processed_modules = [] - report = tools.assertion_report() - # NOTE: Try to also load the modules that have been marked as uninstallable previously... - STATES_TO_LOAD = ['installed', 'to upgrade', 'uninstallable'] - if 'base' in tools.config['update'] or 'all' in tools.config['update']: - cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed')) - - # STEP 1: LOAD BASE (must be done before module dependencies can be computed for later steps) - graph = openerp.modules.graph.Graph() - graph.add_module(cr, 'base', force) - if not graph: - logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)') - raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)')) - processed_modules.extend(load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report)) - - if tools.config['load_language']: - for lang in tools.config['load_language'].split(','): - tools.load_language(cr, lang) - - # STEP 2: Mark other modules to be loaded/updated - if update_module: - modobj = pool.get('ir.module.module') - if ('base' in tools.config['init']) or ('base' in tools.config['update']): - logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list') - modobj.update_list(cr, 1) - - _check_module_names(cr, itertools.chain(tools.config['init'].keys(), tools.config['update'].keys())) - - mods = [k for k in tools.config['init'] if tools.config['init'][k]] - if mods: - ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)]) - if ids: - modobj.button_install(cr, 1, ids) - - mods = [k for k in tools.config['update'] if tools.config['update'][k]] - if mods: - ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)]) - if ids: - modobj.button_upgrade(cr, 1, ids) - - cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base')) - - STATES_TO_LOAD += ['to install'] - - - # STEP 3: Load marked modules (skipping base which was done in STEP 1) - loop_guardrail = 0 - while True: - loop_guardrail += 1 - if loop_guardrail > 100: - raise ValueError('Possible recursive module tree detected, aborting.') - cr.execute("SELECT name from ir_module_module WHERE state IN %s" ,(tuple(STATES_TO_LOAD),)) - - module_list = [name for (name,) in cr.fetchall() if name not in graph] - if not module_list: - break - - new_modules_in_graph = graph.add_modules(cr, module_list, force) - if new_modules_in_graph == 0: - # nothing to load - break - - logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list))) - processed_modules.extend(load_module_graph(cr, graph, status, report=report, skip_modules=processed_modules)) - - # load custom models - cr.execute('select model from ir_model where state=%s', ('manual',)) - for model in cr.dictfetchall(): - pool.get('ir.model').instanciate(cr, 1, model['model'], {}) - - # STEP 4: Finish and cleanup - if processed_modules: - cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""") - for (model, name) in cr.fetchall(): - model_obj = pool.get(model) - if model_obj and not isinstance(model_obj, osv.osv.osv_memory): - logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name)) - - # Temporary warning while we remove access rights on osv_memory objects, as they have - # been replaced by owner-only access rights - cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""") - for (model, name) in cr.fetchall(): - model_obj = pool.get(model) - if isinstance(model_obj, osv.osv.osv_memory): - logger.notifyChannel('init', netsvc.LOG_WARNING, 'In-memory object %s (%s) should not have explicit access rules!' % (model, name)) - - cr.execute("SELECT model from ir_model") - for (model,) in cr.fetchall(): - obj = pool.get(model) - if obj: - obj._check_removed_columns(cr, log=True) - else: - logger.notifyChannel('init', netsvc.LOG_WARNING, "Model %s is referenced but not present in the orm pool!" % model) - - # Cleanup orphan records - pool.get('ir.model.data')._process_end(cr, 1, processed_modules) - - if report.get_report(): - logger.notifyChannel('init', netsvc.LOG_INFO, report) - - for kind in ('init', 'demo', 'update'): - tools.config[kind] = {} - - cr.commit() - if update_module: - # Remove records referenced from ir_model_data for modules to be - # removed (and removed the references from ir_model_data). - cr.execute("select id,name from ir_module_module where state=%s", ('to remove',)) - for mod_id, mod_name in cr.fetchall(): - cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,)) - for rmod, rid in cr.fetchall(): - uid = 1 - rmod_module= pool.get(rmod) - if rmod_module: - # TODO group by module so that we can delete multiple ids in a call - rmod_module.unlink(cr, uid, [rid]) - else: - logger.notifyChannel('init', netsvc.LOG_ERROR, 'Could not locate %s to remove res=%d' % (rmod,rid)) - cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,)) - cr.commit() - - # Remove menu items that are not referenced by any of other - # (child) menu item, ir_values, or ir_model_data. - # This code could be a method of ir_ui_menu. - # TODO: remove menu without actions of children - while True: - cr.execute('''delete from - ir_ui_menu - where - (id not IN (select parent_id from ir_ui_menu where parent_id is not null)) - and - (id not IN (select res_id from ir_values where model='ir.ui.menu')) - and - (id not IN (select res_id from ir_model_data where model='ir.ui.menu'))''') - cr.commit() - if not cr.rowcount: - break - else: - logger.notifyChannel('init', netsvc.LOG_INFO, 'removed %d unused menus' % (cr.rowcount,)) - - # Pretend that modules to be removed are actually uninstalled. - cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',)) - cr.commit() - finally: - cr.close() +from openerp.modules.loading import load_modules # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py new file mode 100644 index 00000000000..c37016628a9 --- /dev/null +++ b/openerp/modules/loading.py @@ -0,0 +1,422 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# Copyright (C) 2010-2011 OpenERP s.a. (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +""" Modules (also called addons) management. + +""" + +import os, sys, imp +from os.path import join as opj +import itertools +import zipimport + +import openerp + +import openerp.osv as osv +import openerp.tools as tools +import openerp.tools.osutil as osutil +from openerp.tools.safe_eval import safe_eval as eval +import openerp.pooler as pooler +from openerp.tools.translate import _ + +import openerp.netsvc as netsvc + +import zipfile +import openerp.release as release + +import re +import base64 +from zipfile import PyZipFile, ZIP_DEFLATED +from cStringIO import StringIO + +import logging + +import openerp.modules.db +import openerp.modules.graph +import openerp.modules.migration + +from openerp.modules.module import \ + get_modules, get_modules_with_version, \ + load_information_from_description_file, \ + get_module_resource, zip_directory, \ + get_module_path, initialize_sys_path, \ + register_module_classes, init_module_models + +logger = netsvc.Logger() + + +def open_openerp_namespace(): + # See comment for open_openerp_namespace. + if openerp.conf.deprecation.open_openerp_namespace: + for k, v in list(sys.modules.items()): + if k.startswith('openerp.') and sys.modules.get(k[8:]) is None: + sys.modules[k[8:]] = v + + +def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, report=None): + """Migrates+Updates or Installs all module nodes from ``graph`` + :param graph: graph of module nodes to load + :param status: status dictionary for keeping track of progress + :param perform_checks: whether module descriptors should be checked for validity (prints warnings + for same cases, and even raise osv_except if certificate is invalid) + :param skip_modules: optional list of module names (packages) which have previously been loaded and can be skipped + :return: list of modules that were installed or updated + """ + def process_sql_file(cr, fp): + queries = fp.read().split(';') + for query in queries: + new_query = ' '.join(query.split()) + if new_query: + cr.execute(new_query) + + def load_init_xml(cr, m, idref, mode): + _load_data(cr, m, idref, mode, 'init_xml') + + def load_update_xml(cr, m, idref, mode): + _load_data(cr, m, idref, mode, 'update_xml') + + def load_demo_xml(cr, m, idref, mode): + _load_data(cr, m, idref, mode, 'demo_xml') + + def load_data(cr, module_name, idref, mode): + _load_data(cr, module_name, idref, mode, 'data') + + def load_demo(cr, module_name, idref, mode): + _load_data(cr, module_name, idref, mode, 'demo') + + def load_test(cr, module_name, idref, mode): + cr.commit() + if not tools.config.options['test_disable']: + try: + _load_data(cr, module_name, idref, mode, 'test') + except Exception, e: + logging.getLogger('test').exception('Tests failed to execute in module %s', module_name) + finally: + if tools.config.options['test_commit']: + cr.commit() + else: + cr.rollback() + + def _load_data(cr, module_name, idref, mode, kind): + """ + + kind: data, demo, test, init_xml, update_xml, demo_xml. + + noupdate is False, unless it is demo data or it is csv data in + init mode. + + """ + for filename in package.data[kind]: + log = logging.getLogger('init') + log.info("module %s: loading %s", module_name, filename) + _, ext = os.path.splitext(filename) + pathname = os.path.join(module_name, filename) + fp = tools.file_open(pathname) + noupdate = False + if kind in ('demo', 'demo_xml'): + noupdate = True + try: + if ext == '.csv': + if kind in ('init', 'init_xml'): + noupdate = True + tools.convert_csv_import(cr, module_name, pathname, fp.read(), idref, mode, noupdate) + elif ext == '.sql': + process_sql_file(cr, fp) + elif ext == '.yml': + tools.convert_yaml_import(cr, module_name, fp, idref, mode, noupdate) + else: + tools.convert_xml_import(cr, module_name, fp, idref, mode, noupdate, report) + finally: + fp.close() + + if status is None: + status = {} + + processed_modules = [] + statusi = 0 + pool = pooler.get_pool(cr.dbname) + migrations = openerp.modules.migration.MigrationManager(cr, graph) + logger.notifyChannel('init', netsvc.LOG_DEBUG, 'loading %d packages..' % len(graph)) + + # register, instanciate and initialize models for each modules + for package in graph: + if skip_modules and package.name in skip_modules: + continue + logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name) + migrations.migrate_module(package, 'pre') + register_module_classes(package.name) + models = pool.instanciate(package.name, cr) + if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'): + init_module_models(cr, package.name, models) + cr.commit() + + # load data for each modules + modobj = pool.get('ir.module.module') + for package in graph: + status['progress'] = (float(statusi)+0.1) / len(graph) + m = package.name + mid = package.id + + if skip_modules and m in skip_modules: + continue + + if perform_checks: + modobj.check(cr, 1, [mid]) + + idref = {} + status['progress'] = (float(statusi)+0.4) / len(graph) + + mode = 'update' + if hasattr(package, 'init') or package.state == 'to install': + mode = 'init' + + if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'): + if package.state=='to upgrade': + # upgrading the module information + modobj.write(cr, 1, [mid], modobj.get_values_from_terp(package.data)) + load_init_xml(cr, m, idref, mode) + load_update_xml(cr, m, idref, mode) + load_data(cr, m, idref, mode) + if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'): + status['progress'] = (float(statusi)+0.75) / len(graph) + load_demo_xml(cr, m, idref, mode) + load_demo(cr, m, idref, mode) + cr.execute('update ir_module_module set demo=%s where id=%s', (True, mid)) + + # launch tests only in demo mode, as most tests will depend + # on demo data. Other tests can be added into the regular + # 'data' section, but should probably not alter the data, + # as there is no rollback. + load_test(cr, m, idref, mode) + + processed_modules.append(package.name) + + migrations.migrate_module(package, 'post') + + ver = release.major_version + '.' + package.data['version'] + # Set new modules and dependencies + modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver}) + cr.commit() + # Update translations for all installed languages + modobj.update_translations(cr, 1, [mid], None) + cr.commit() + + package.state = 'installed' + for kind in ('init', 'demo', 'update'): + if hasattr(package, kind): + delattr(package, kind) + + statusi += 1 + + cr.commit() + + return processed_modules + +def _check_module_names(cr, module_names): + mod_names = set(module_names) + if 'base' in mod_names: + # ignore dummy 'all' module + if 'all' in mod_names: + mod_names.remove('all') + if mod_names: + cr.execute("SELECT count(id) AS count FROM ir_module_module WHERE name in %s", (tuple(mod_names),)) + if cr.dictfetchone()['count'] != len(mod_names): + # find out what module name(s) are incorrect: + cr.execute("SELECT name FROM ir_module_module") + incorrect_names = mod_names.difference([x['name'] for x in cr.dictfetchall()]) + logging.getLogger('init').warning('invalid module names, ignored: %s', ", ".join(incorrect_names)) + +def load_modules(db, force_demo=False, status=None, update_module=False): + # TODO status['progress'] reporting is broken: used twice (and reset each + # time to zero) in load_module_graph, not fine-grained enough. + # It should be a method exposed by the pool. + + initialize_sys_path() + + open_openerp_namespace() + + cr = db.cursor() + if cr: + cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='ir_module_module'") + if len(cr.fetchall())==0: + logger.notifyChannel("init", netsvc.LOG_INFO, "init db") + openerp.modules.db.initialize(cr) + tools.config["init"]["all"] = 1 + tools.config['update']['all'] = 1 + if not tools.config['without_demo']: + tools.config["demo"]['all'] = 1 + force = [] + if force_demo: + force.append('demo') + + # This is a brand new pool, just created in pooler.get_db_and_pool() + pool = pooler.get_pool(cr.dbname) + + try: + processed_modules = [] + report = tools.assertion_report() + # NOTE: Try to also load the modules that have been marked as uninstallable previously... + STATES_TO_LOAD = ['installed', 'to upgrade', 'uninstallable'] + if 'base' in tools.config['update'] or 'all' in tools.config['update']: + cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed')) + + # STEP 1: LOAD BASE (must be done before module dependencies can be computed for later steps) + graph = openerp.modules.graph.Graph() + graph.add_module(cr, 'base', force) + if not graph: + logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)') + raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)')) + processed_modules.extend(load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report)) + + if tools.config['load_language']: + for lang in tools.config['load_language'].split(','): + tools.load_language(cr, lang) + + # STEP 2: Mark other modules to be loaded/updated + if update_module: + modobj = pool.get('ir.module.module') + if ('base' in tools.config['init']) or ('base' in tools.config['update']): + logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list') + modobj.update_list(cr, 1) + + _check_module_names(cr, itertools.chain(tools.config['init'].keys(), tools.config['update'].keys())) + + mods = [k for k in tools.config['init'] if tools.config['init'][k]] + if mods: + ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)]) + if ids: + modobj.button_install(cr, 1, ids) + + mods = [k for k in tools.config['update'] if tools.config['update'][k]] + if mods: + ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)]) + if ids: + modobj.button_upgrade(cr, 1, ids) + + cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base')) + + STATES_TO_LOAD += ['to install'] + + + # STEP 3: Load marked modules (skipping base which was done in STEP 1) + loop_guardrail = 0 + while True: + loop_guardrail += 1 + if loop_guardrail > 100: + raise ValueError('Possible recursive module tree detected, aborting.') + cr.execute("SELECT name from ir_module_module WHERE state IN %s" ,(tuple(STATES_TO_LOAD),)) + + module_list = [name for (name,) in cr.fetchall() if name not in graph] + if not module_list: + break + + new_modules_in_graph = graph.add_modules(cr, module_list, force) + if new_modules_in_graph == 0: + # nothing to load + break + + logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list))) + processed_modules.extend(load_module_graph(cr, graph, status, report=report, skip_modules=processed_modules)) + + # load custom models + cr.execute('select model from ir_model where state=%s', ('manual',)) + for model in cr.dictfetchall(): + pool.get('ir.model').instanciate(cr, 1, model['model'], {}) + + # STEP 4: Finish and cleanup + if processed_modules: + cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""") + for (model, name) in cr.fetchall(): + model_obj = pool.get(model) + if model_obj and not isinstance(model_obj, osv.osv.osv_memory): + logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name)) + + # Temporary warning while we remove access rights on osv_memory objects, as they have + # been replaced by owner-only access rights + cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""") + for (model, name) in cr.fetchall(): + model_obj = pool.get(model) + if isinstance(model_obj, osv.osv.osv_memory): + logger.notifyChannel('init', netsvc.LOG_WARNING, 'In-memory object %s (%s) should not have explicit access rules!' % (model, name)) + + cr.execute("SELECT model from ir_model") + for (model,) in cr.fetchall(): + obj = pool.get(model) + if obj: + obj._check_removed_columns(cr, log=True) + else: + logger.notifyChannel('init', netsvc.LOG_WARNING, "Model %s is referenced but not present in the orm pool!" % model) + + # Cleanup orphan records + pool.get('ir.model.data')._process_end(cr, 1, processed_modules) + + if report.get_report(): + logger.notifyChannel('init', netsvc.LOG_INFO, report) + + for kind in ('init', 'demo', 'update'): + tools.config[kind] = {} + + cr.commit() + if update_module: + # Remove records referenced from ir_model_data for modules to be + # removed (and removed the references from ir_model_data). + cr.execute("select id,name from ir_module_module where state=%s", ('to remove',)) + for mod_id, mod_name in cr.fetchall(): + cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,)) + for rmod, rid in cr.fetchall(): + uid = 1 + rmod_module= pool.get(rmod) + if rmod_module: + # TODO group by module so that we can delete multiple ids in a call + rmod_module.unlink(cr, uid, [rid]) + else: + logger.notifyChannel('init', netsvc.LOG_ERROR, 'Could not locate %s to remove res=%d' % (rmod,rid)) + cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,)) + cr.commit() + + # Remove menu items that are not referenced by any of other + # (child) menu item, ir_values, or ir_model_data. + # This code could be a method of ir_ui_menu. + # TODO: remove menu without actions of children + while True: + cr.execute('''delete from + ir_ui_menu + where + (id not IN (select parent_id from ir_ui_menu where parent_id is not null)) + and + (id not IN (select res_id from ir_values where model='ir.ui.menu')) + and + (id not IN (select res_id from ir_model_data where model='ir.ui.menu'))''') + cr.commit() + if not cr.rowcount: + break + else: + logger.notifyChannel('init', netsvc.LOG_INFO, 'removed %d unused menus' % (cr.rowcount,)) + + # Pretend that modules to be removed are actually uninstalled. + cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',)) + cr.commit() + finally: + cr.close() + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 09079b0102047551cf45ecb0c2f9fec770c2222f Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 12 May 2011 10:46:46 +0200 Subject: [PATCH 32/35] [REF] pooler code goes in openerp.registry.manager. bzr revid: vmt@openerp.com-20110512084646-k0s6dlkpbz5s4mcs --- openerp/__init__.py | 1 + openerp/pooler.py | 74 ++++++++++-------------- openerp/registry/__init__.py | 29 ++++++++++ openerp/registry/manager.py | 108 +++++++++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 45 deletions(-) create mode 100644 openerp/registry/__init__.py create mode 100644 openerp/registry/manager.py diff --git a/openerp/__init__.py b/openerp/__init__.py index a3cd4b3c00a..63016a520ee 100644 --- a/openerp/__init__.py +++ b/openerp/__init__.py @@ -32,6 +32,7 @@ import netsvc import osv import pooler import pychart +import registry import release import report import run_tests diff --git a/openerp/pooler.py b/openerp/pooler.py index fb47a948752..c3b7b6f2eba 100644 --- a/openerp/pooler.py +++ b/openerp/pooler.py @@ -19,66 +19,50 @@ # ############################################################################## -import sql_db +""" Functions kept for backward compatibility. + + They are simple wrappers around a global RegistryManager methods. + +""" + +from openerp.registry.manager import RegistryManager + +_Registries = None + + +def ensure_registries(): + global _Registries + if _Registries is None: + _Registries = RegistryManager() -# Mapping between db name and osv_pool. -# Accessed through the functions below. -pool_dic = {} def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False, pooljobs=True): - """Return a database connection and an initialized osv_pool.""" - - db = sql_db.db_connect(db_name) - - if db_name in pool_dic: - pool = pool_dic[db_name] - else: - import openerp.modules - import openerp.osv.osv as osv_osv - pool = osv_osv.osv_pool() - - # Initializing an osv_pool will call general code which will in turn - # call get_db_and_pool (this function) to obtain the osv_pool being - # initialized. Make it available in the pool_dic then remove it if - # an exception is raised. - pool_dic[db_name] = pool - try: - openerp.modules.load_modules(db, force_demo, status, update_module) - except Exception: - del pool_dic[db_name] - raise - - cr = db.cursor() - try: - pool.init_set(cr, False) - pool.get('ir.actions.report.xml').register_all(cr) - cr.commit() - finally: - cr.close() - - if pooljobs: - pool.get('ir.cron').restart(db.dbname) - return db, pool + """Create and return a database connection and a newly initialized registry.""" + ensure_registries() + bound_registry = _Registries.get(db_name, force_demo, status, update_module, pooljobs) + return bound_registry.db, bound_registry.registry def delete_pool(db_name): - """Delete an existing osv_pool.""" - if db_name in pool_dic: - del pool_dic[db_name] + """Delete an existing registry.""" + ensure_registries() + _Registries.delete(db_name) + def restart_pool(db_name, force_demo=False, status=None, update_module=False): - """Delete an existing osv_pool and return a database connection and a newly initialized osv_pool.""" - delete_pool(db_name) - return get_db_and_pool(db_name, force_demo, status, update_module=update_module) + """Delete an existing registry and return a database connection and a newly initialized registry.""" + ensure_registries() + bound_registry = _Registries.new(db_name, force_demo, status, update_module, pooljobs) + return bound_registry.db, bound_registry.registry def get_db(db_name): - """Return a database connection. The corresponding osv_pool is initialize.""" + """Return a database connection. The corresponding registry is initialized.""" return get_db_and_pool(db_name)[0] def get_pool(db_name, force_demo=False, status=None, update_module=False): - """Return an osv_pool.""" + """Return a model registry.""" return get_db_and_pool(db_name, force_demo, status, update_module)[1] # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/registry/__init__.py b/openerp/registry/__init__.py new file mode 100644 index 00000000000..5ac650bc771 --- /dev/null +++ b/openerp/registry/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2011 OpenERP s.a. (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +""" Modules registries. + +""" + +import manager + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/registry/manager.py b/openerp/registry/manager.py new file mode 100644 index 00000000000..ff2cc235bb2 --- /dev/null +++ b/openerp/registry/manager.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +""" Model registries manager. + + The manager is responsible for creation and deletion of bound model + registries (essentially database connection/model registry pairs). + +""" + +import openerp.sql_db + + +class BoundRegistry(object): + """ Model registry/database connection pair.""" + def __init__(self, db, registry): + self.db = db + self.registry = registry + + +class RegistryManager(object): + + + # TODO maybe should receive the addons paths + def __init__(self): + # Mapping between db name and bound model registry. + # Accessed through the methods below. + self.bound_registries = {} + + + def get(self, db_name, force_demo=False, status=None, update_module=False, + pooljobs=True): + """ Return a bound registry for a given database name.""" + + if db_name in self.bound_registries: + bound_registry = self.bound_registries[db_name] + else: + bound_registry = self.new(db_name, force_demo, status, + update_module, pooljobs) + return bound_registry + + + def new(self, db_name, force_demo=False, status=None, + update_module=False, pooljobs=True): + """ Create and return a new bound registry for a given database name. + + The (possibly) previous bound registry for that database name is + discarded. + + """ + + import openerp.modules + import openerp.osv.osv as osv_osv + db = openerp.sql_db.db_connect(db_name) + pool = osv_osv.osv_pool() + + # Initializing a registry will call general code which will in turn + # call registries.get (this object) to obtain the registry being + # initialized. Make it available in the bound_registries dictionary + # then remove it if an exception is raised. + self.delete(db_name) + bound_registry = BoundRegistry(db, pool) + self.bound_registries[db_name] = bound_registry + try: + # This should be a method on BoundRegistry + openerp.modules.load_modules(db, force_demo, status, update_module) + except Exception: + del self.bound_registries[db_name] + raise + + cr = db.cursor() + try: + pool.init_set(cr, False) + pool.get('ir.actions.report.xml').register_all(cr) + cr.commit() + finally: + cr.close() + + if pooljobs: + pool.get('ir.cron').restart(db.dbname) + + return bound_registry + + + def delete(self, db_name): + if db_name in self.bound_registries: + del self.bound_registries[db_name] + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 6c051106ae84b6510c2245b6fa44f604863d76b0 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 13 May 2011 11:37:01 +0200 Subject: [PATCH 33/35] [FIX] fix bad copy/paste. bzr revid: vmt@openerp.com-20110513093701-1lgbe077o72djjbt --- openerp/pooler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/pooler.py b/openerp/pooler.py index c3b7b6f2eba..2a038ba833c 100644 --- a/openerp/pooler.py +++ b/openerp/pooler.py @@ -52,7 +52,7 @@ def delete_pool(db_name): def restart_pool(db_name, force_demo=False, status=None, update_module=False): """Delete an existing registry and return a database connection and a newly initialized registry.""" ensure_registries() - bound_registry = _Registries.new(db_name, force_demo, status, update_module, pooljobs) + bound_registry = _Registries.new(db_name, force_demo, status, update_module, True) return bound_registry.db, bound_registry.registry From a09b91d8fdb88a1e9789f548670cb41d0ff98d6c Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 17 May 2011 11:18:22 +0200 Subject: [PATCH 34/35] [REF] simplified init_db/load_modules: - removed unnecessary call to openerp.modules.db.initialize() in openerp/service/web_services.py as the pooler.restart_pool() call just next after is already doing it. - made the try/finally section bigger in openerp/modules/loading.py, to inlcude the first cr.execute. - abstracted the test to check if a database is initialized. - removed unnecessary "if cr:" as the cr is nevertheless used after that. bzr revid: vmt@openerp.com-20110517091822-pjtw6sc1s5assbr5 --- openerp/modules/db.py | 9 +++++++++ openerp/modules/loading.py | 17 ++++++++--------- openerp/service/web_services.py | 5 ----- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/openerp/modules/db.py b/openerp/modules/db.py index 261b39ffb31..25ce7283c97 100644 --- a/openerp/modules/db.py +++ b/openerp/modules/db.py @@ -22,6 +22,15 @@ import openerp.modules +def is_initialized(cr): + """ Check if a database has been initialized for the ORM. + + The database can be initialized with the 'initialize' function below. + + """ + cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='ir_module_module'") + return len(cr.fetchall()) > 0 + def initialize(cr): """ Initialize a database with for the ORM. diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index c37016628a9..17ae2dce3ef 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -254,24 +254,23 @@ def load_modules(db, force_demo=False, status=None, update_module=False): open_openerp_namespace() + force = [] + if force_demo: + force.append('demo') + cr = db.cursor() - if cr: - cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='ir_module_module'") - if len(cr.fetchall())==0: + try: + if not openerp.modules.db.is_initialized(cr): logger.notifyChannel("init", netsvc.LOG_INFO, "init db") openerp.modules.db.initialize(cr) tools.config["init"]["all"] = 1 tools.config['update']['all'] = 1 if not tools.config['without_demo']: tools.config["demo"]['all'] = 1 - force = [] - if force_demo: - force.append('demo') - # This is a brand new pool, just created in pooler.get_db_and_pool() - pool = pooler.get_pool(cr.dbname) + # This is a brand new pool, just created in pooler.get_db_and_pool() + pool = pooler.get_pool(cr.dbname) - try: processed_modules = [] report = tools.assertion_report() # NOTE: Try to also load the modules that have been marked as uninstallable previously... diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index 8806e1b4f99..4f6ab758851 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -92,12 +92,7 @@ class db(netsvc.ExportService): cr = None try: serv.actions[id]['progress'] = 0 - cr = sql_db.db_connect(db_name).cursor() - openerp.modules.db.initialize(cr) tools.config['lang'] = lang - cr.commit() - cr.close() - cr = None pool = pooler.restart_pool(db_name, demo, serv.actions[id], update_module=True)[1] From 16ac84af68d91b177394a2249f8b866e45ce3de3 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 19 May 2011 09:39:35 +0200 Subject: [PATCH 35/35] [REF] moved openerp/registry/manager.py => openerp/modules/registry.py - Maybe it should be called registries.py... Argh, consistency... bzr revid: vmt@openerp.com-20110519073935-81kwuhlqcxxw1z32 --- openerp/__init__.py | 1 - .../manager.py => modules/registry.py} | 10 ++++--- openerp/pooler.py | 2 +- openerp/registry/__init__.py | 29 ------------------- 4 files changed, 7 insertions(+), 35 deletions(-) rename openerp/{registry/manager.py => modules/registry.py} (94%) delete mode 100644 openerp/registry/__init__.py diff --git a/openerp/__init__.py b/openerp/__init__.py index 63016a520ee..a3cd4b3c00a 100644 --- a/openerp/__init__.py +++ b/openerp/__init__.py @@ -32,7 +32,6 @@ import netsvc import osv import pooler import pychart -import registry import release import report import run_tests diff --git a/openerp/registry/manager.py b/openerp/modules/registry.py similarity index 94% rename from openerp/registry/manager.py rename to openerp/modules/registry.py index ff2cc235bb2..3b11219d5a5 100644 --- a/openerp/registry/manager.py +++ b/openerp/modules/registry.py @@ -19,10 +19,7 @@ # ############################################################################## -""" Model registries manager. - - The manager is responsible for creation and deletion of bound model - registries (essentially database connection/model registry pairs). +""" Models registries. """ @@ -37,7 +34,12 @@ class BoundRegistry(object): class RegistryManager(object): + """ Model registries manager. + The manager is responsible for creation and deletion of bound model + registries (essentially database connection/model registry pairs). + + """ # TODO maybe should receive the addons paths def __init__(self): diff --git a/openerp/pooler.py b/openerp/pooler.py index 2a038ba833c..aace66a5c84 100644 --- a/openerp/pooler.py +++ b/openerp/pooler.py @@ -25,7 +25,7 @@ """ -from openerp.registry.manager import RegistryManager +from openerp.modules.registry import RegistryManager _Registries = None diff --git a/openerp/registry/__init__.py b/openerp/registry/__init__.py deleted file mode 100644 index 5ac650bc771..00000000000 --- a/openerp/registry/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2011 OpenERP s.a. (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -""" Modules registries. - -""" - -import manager - - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: