From 2e6a589f417af48e3322955ffff6e94b6d8a24e3 Mon Sep 17 00:00:00 2001 From: xmo-odoo Date: Thu, 27 Apr 2017 13:59:33 +0200 Subject: [PATCH] [FIX] builtins removed from Python 3 * Reverse wrapper courtesy of @rco-odoo's original P3 branch * thin compat module stripped down from werkzeug (to augment as needed) issue 8530 --- addons/account/models/account.py | 6 +- .../account/models/account_bank_statement.py | 6 +- addons/auth_signup/models/res_partner.py | 3 +- addons/calendar/models/calendar.py | 45 +++--- addons/calendar/tests/test_calendar.py | 47 ++++++ addons/gamification/models/goal.py | 5 +- addons/hr_payroll/tests/test_payslip_flow.py | 4 +- addons/hw_escpos/escpos/escpos.py | 3 +- addons/l10n_ch/models/mail_template.py | 3 +- .../tests/test_payment_advice.py | 2 +- addons/mail/models/mail_template.py | 12 +- addons/mail/models/mail_thread.py | 3 +- addons/mail/models/res_partner.py | 11 +- addons/mail/models/res_users.py | 3 +- addons/mail/wizard/mail_compose_message.py | 5 +- addons/payment_ogone/models/payment.py | 2 +- addons/product/models/product.py | 4 +- addons/product/models/product_template.py | 3 +- addons/report/models/report.py | 2 +- .../tests/test_report_intrastat.py | 2 +- addons/resource/tests/test_resource.py | 10 +- addons/sale/models/account_invoice.py | 8 +- addons/stock/models/procurement.py | 5 +- addons/stock/models/product.py | 7 +- addons/web_editor/models/ir_ui_view.py | 3 +- addons/website/models/ir_ui_view.py | 5 +- addons/website/models/website.py | 9 +- addons/website/tests/test_crawl.py | 4 +- doc/python3.rst | 149 ++++++++++++++++-- odoo/addons/base/ir/ir_model.py | 3 +- odoo/addons/base/ir/ir_qweb/ir_qweb.py | 3 +- odoo/addons/base/ir/ir_qweb/qweb.py | 5 +- odoo/addons/base/ir/ir_translation.py | 3 +- odoo/addons/base/ir/ir_ui_menu.py | 4 +- odoo/addons/base/ir/ir_ui_view.py | 6 +- odoo/addons/base/res/ir_property.py | 3 +- odoo/addons/base/tests/test_api.py | 6 +- odoo/addons/base/tests/test_expression.py | 3 +- odoo/addons/base/tests/test_float.py | 13 +- odoo/addons/base/tests/test_ir_sequence.py | 4 +- .../base/tests/test_ir_sequence_date_range.py | 8 +- odoo/addons/base/tests/test_misc.py | 4 +- odoo/addons/base/tests/test_orm.py | 4 +- odoo/addons/base/tests/test_res_config.py | 3 +- odoo/addons/test_limits/models.py | 4 +- .../test_new_api/tests/test_attributes.py | 3 +- odoo/addons/test_pylint/tests/test_pylint.py | 15 ++ odoo/fields.py | 5 +- odoo/http.py | 19 +-- odoo/models.py | 9 +- odoo/modules/graph.py | 2 +- odoo/modules/migration.py | 3 +- odoo/modules/module.py | 2 +- odoo/osv/expression.py | 3 +- odoo/service/model.py | 5 +- odoo/service/server.py | 2 +- odoo/sql_db.py | 8 +- odoo/tests/common.py | 2 +- odoo/tools/config.py | 2 +- odoo/tools/convert.py | 6 +- odoo/tools/float_utils.py | 16 +- odoo/tools/graph.py | 2 +- odoo/tools/image.py | 4 +- odoo/tools/import_email.py | 2 +- odoo/tools/misc.py | 46 +++++- odoo/tools/pycompat.py | 31 ++++ odoo/tools/safe_eval.py | 9 +- odoo/tools/test_reports.py | 6 +- odoo/tools/translate.py | 6 +- setup.py | 2 +- setup/package.py | 2 +- setup/win32/win32_service.py | 2 +- setup/win32/win32_setup.py | 2 +- 73 files changed, 464 insertions(+), 199 deletions(-) create mode 100644 odoo/tools/pycompat.py diff --git a/addons/account/models/account.py b/addons/account/models/account.py index d9f6b83fb07..a626a410582 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -5,7 +5,7 @@ import math from odoo.osv import expression from odoo.tools.float_utils import float_round as round -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, pycompat from odoo.exceptions import UserError, ValidationError from odoo import api, fields, models, _ @@ -383,7 +383,7 @@ class AccountJournal(models.Model): account_code_prefix = company.bank_account_code_prefix or '' else: account_code_prefix = company.cash_account_code_prefix or company.bank_account_code_prefix or '' - for num in xrange(1, 100): + for num in pycompat.range(1, 100): new_code = str(account_code_prefix.ljust(code_digits - 1, '0')) + str(num) rec = self.env['account.account'].search([('code', '=', new_code), ('company_id', '=', company.id)], limit=1) if not rec: @@ -412,7 +412,7 @@ class AccountJournal(models.Model): if not vals.get('code'): journal_code_base = (vals['type'] == 'cash' and 'CSH' or 'BNK') journals = self.env['account.journal'].search([('code', 'like', journal_code_base + '%'), ('company_id', '=', company_id)]) - for num in xrange(1, 100): + for num in pycompat.range(1, 100): # journal_code has a maximal size of 5, hence we can enforce the boundary num < 100 journal_code = journal_code_base + str(num) if journal_code not in journals.mapped('code'): diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 1ee32068a00..2d7569f2c56 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -2,7 +2,7 @@ from odoo import api, fields, models, _ from odoo.osv import expression -from odoo.tools import float_is_zero +from odoo.tools import float_is_zero, pycompat from odoo.tools import float_compare, float_round from odoo.tools.misc import formatLang from odoo.exceptions import UserError, ValidationError @@ -874,10 +874,10 @@ class AccountBankStatementLine(models.Model): for aml_dict in counterpart_aml_dicts: if aml_dict['move_line'].reconciled: raise UserError(_('A selected move line was already reconciled.')) - if isinstance(aml_dict['move_line'], (int, long)): + if isinstance(aml_dict['move_line'], pycompat.integer_types): aml_dict['move_line'] = aml_obj.browse(aml_dict['move_line']) for aml_dict in (counterpart_aml_dicts + new_aml_dicts): - if aml_dict.get('tax_ids') and aml_dict['tax_ids'] and isinstance(aml_dict['tax_ids'][0], (int, long)): + if aml_dict.get('tax_ids') and isinstance(aml_dict['tax_ids'][0], pycompat.integer_types): # Transform the value in the format required for One2many and Many2many fields aml_dict['tax_ids'] = map(lambda id: (4, id, None), aml_dict['tax_ids']) diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py index 6dbf14188d8..99e0b98c5ea 100644 --- a/addons/auth_signup/models/res_partner.py +++ b/addons/auth_signup/models/res_partner.py @@ -8,6 +8,7 @@ from datetime import datetime, timedelta from urlparse import urljoin from odoo import api, fields, models, _ +from odoo.tools import pycompat class SignupError(Exception): @@ -16,7 +17,7 @@ class SignupError(Exception): def random_token(): # the token has an entropy of about 120 bits (6 bits/char * 20 chars) chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - return ''.join(random.SystemRandom().choice(chars) for i in xrange(20)) + return ''.join(random.SystemRandom().choice(chars) for _ in pycompat.range(20)) def now(**kwargs): dt = datetime.now() + timedelta(**kwargs) diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index f4380d12e66..98f5e5c95d9 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -17,7 +17,7 @@ import uuid from odoo import api, fields, models from odoo import tools from odoo.tools.translate import _ -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, pycompat from odoo.exceptions import UserError, ValidationError @@ -47,7 +47,7 @@ def calendar_id2real_id(calendar_id=None, with_date=False): def get_real_ids(ids): - if isinstance(ids, (basestring, int, long)): + if isinstance(ids, (basestring, pycompat.integer_types)): return calendar_id2real_id(ids) if isinstance(ids, (list, tuple)): @@ -62,6 +62,15 @@ def is_calendar_id(record_id): return len(str(record_id).split('-')) != 1 +SORT_ALIASES = { + 'start': 'sort_start', + 'start_date': 'sort_start', + 'start_datetime': 'sort_start', +} +def sort_remap(f): + return SORT_ALIASES.get(f, f) + + class Contacts(models.Model): _name = 'calendar.contacts' @@ -1062,23 +1071,17 @@ class Meeting(models.Model): continue result_data.append(meeting.get_search_fields(order_fields, r_date=r_start_date)) - if order_fields: - uniq = lambda it: collections.OrderedDict((id(x), x) for x in it).values() - - def comparer(left, right): - for fn, mult in comparers: - result = cmp(fn(left), fn(right)) - if result: - return mult * result - return 0 - - sort_params = [key.split()[0] if key[-4:].lower() != 'desc' else '-%s' % key.split()[0] for key in (order or self._order).split(',')] - sort_params = uniq([comp if comp not in ['start', 'start_date', 'start_datetime'] else 'sort_start' for comp in sort_params]) - sort_params = uniq([comp if comp not in ['-start', '-start_date', '-start_datetime'] else '-sort_start' for comp in sort_params]) - comparers = [((itemgetter(col[1:]), -1) if col[0] == '-' else (itemgetter(col), 1)) for col in sort_params] - ids = [r['id'] for r in sorted(result_data, cmp=comparer)] - - return ids + # seq of (field, should_reverse) + sort_spec = list(tools.unique( + (sort_remap(key.split()[0]), key.lower().endswith(' desc')) + for key in (order or self._order).split(',') + )) + def key(record): + return [ + tools.Reverse(record[name]) if desc else record[name] + for name, desc in sort_spec + ] + return [r['id'] for r in sorted(result_data, key=key)] @api.multi def _rrule_serialize(self): @@ -1154,7 +1157,7 @@ class Meeting(models.Model): data['final_date'] = rule._until and rule._until.strftime(DEFAULT_SERVER_DATETIME_FORMAT) #repeat weekly if rule._byweekday: - for i in xrange(0, 7): + for i in pycompat.range(0, 7): if i in rule._byweekday: data[day_list[i]] = True data['rrule_type'] = 'weekly' @@ -1466,7 +1469,7 @@ class Meeting(models.Model): for calendar_id, real_id in select: res = real_data[real_id].copy() ls = calendar_id2real_id(calendar_id, with_date=res and res.get('duration', 0) > 0 and res.get('duration') or 1) - if not isinstance(ls, (basestring, int, long)) and len(ls) >= 2: + if not isinstance(ls, (basestring, pycompat.integer_types)) and len(ls) >= 2: res['start'] = ls[1] res['stop'] = ls[2] diff --git a/addons/calendar/tests/test_calendar.py b/addons/calendar/tests/test_calendar.py index 3604352ace5..0b986f18bb3 100644 --- a/addons/calendar/tests/test_calendar.py +++ b/addons/calendar/tests/test_calendar.py @@ -97,3 +97,50 @@ class TestCalendar(TransactionCase): self.assertEqual(calendar_event_sprint_review.month_by, 'day', 'rrule_type should be mothly') self.assertEqual(calendar_event_sprint_review.byday, '1', 'rrule_type should be mothly') self.assertEqual(calendar_event_sprint_review.week_list, 'MO', 'rrule_type should be mothly') + + def test_event_order(self): + """ check the ordering of events when searching """ + def create_event(name, date): + return self.CalendarEvent.create({ + 'name': name, + 'start': date + ' 12:00:00', + 'stop': date + ' 14:00:00', + 'duration': 2.0, + }) + foo1 = create_event('foo', '2011-04-01') + foo2 = create_event('foo', '2011-06-01') + bar1 = create_event('bar', '2011-05-01') + bar2 = create_event('bar', '2011-06-01') + domain = [('id', 'in', (foo1 + foo2 + bar1 + bar2).ids)] + + # sort them by name only + events = self.CalendarEvent.search(domain, order='name') + self.assertEqual(events.mapped('name'), ['bar', 'bar', 'foo', 'foo']) + events = self.CalendarEvent.search(domain, order='name desc') + self.assertEqual(events.mapped('name'), ['foo', 'foo', 'bar', 'bar']) + + # sort them by start date only + events = self.CalendarEvent.search(domain, order='start') + self.assertEqual(events.mapped('start'), (foo1 + bar1 + foo2 + bar2).mapped('start')) + events = self.CalendarEvent.search(domain, order='start desc') + self.assertEqual(events.mapped('start'), (foo2 + bar2 + bar1 + foo1).mapped('start')) + + # sort them by name then start date + events = self.CalendarEvent.search(domain, order='name asc, start asc') + self.assertEqual(list(events), [bar1, bar2, foo1, foo2]) + events = self.CalendarEvent.search(domain, order='name asc, start desc') + self.assertEqual(list(events), [bar2, bar1, foo2, foo1]) + events = self.CalendarEvent.search(domain, order='name desc, start asc') + self.assertEqual(list(events), [foo1, foo2, bar1, bar2]) + events = self.CalendarEvent.search(domain, order='name desc, start desc') + self.assertEqual(list(events), [foo2, foo1, bar2, bar1]) + + # sort them by start date then name + events = self.CalendarEvent.search(domain, order='start asc, name asc') + self.assertEqual(list(events), [foo1, bar1, bar2, foo2]) + events = self.CalendarEvent.search(domain, order='start asc, name desc') + self.assertEqual(list(events), [foo1, bar1, foo2, bar2]) + events = self.CalendarEvent.search(domain, order='start desc, name asc') + self.assertEqual(list(events), [bar2, foo2, bar1, foo1]) + events = self.CalendarEvent.search(domain, order='start desc, name desc') + self.assertEqual(list(events), [foo2, bar2, bar1, foo1]) diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py index 1e9975ea97f..5a306ae9a33 100644 --- a/addons/gamification/models/goal.py +++ b/addons/gamification/models/goal.py @@ -7,6 +7,7 @@ from datetime import date, datetime, timedelta from odoo import api, fields, models, _, exceptions from odoo.osv import expression +from odoo.tools import pycompat from odoo.tools.safe_eval import safe_eval _logger = logging.getLogger(__name__) @@ -281,7 +282,7 @@ class Goal(models.Model): safe_eval(code, cxt, mode="exec", nocopy=True) # the result of the evaluated codeis put in the 'result' local variable, propagated to the context result = cxt.get('result') - if result is not None and isinstance(result, (float, int, long)): + if result is not None and isinstance(result, (float, pycompat.integer_types)): goals_to_write.update(goal._get_write_values(result)) else: _logger.error( @@ -322,7 +323,7 @@ class Goal(models.Model): for goal in [g for g in goals if g.id in query_goals]: for user_value in user_values: queried_value = field_name in user_value and user_value[field_name] or False - if isinstance(queried_value, tuple) and len(queried_value) == 2 and isinstance(queried_value[0], (int, long)): + if isinstance(queried_value, tuple) and len(queried_value) == 2 and isinstance(queried_value[0], pycompat.integer_types): queried_value = queried_value[0] if queried_value == query_goals[goal.id]: new_value = user_value.get(field_name+'_count', goal.current) diff --git a/addons/hr_payroll/tests/test_payslip_flow.py b/addons/hr_payroll/tests/test_payslip_flow.py index 6fa71dbe27f..7ab0fad275a 100644 --- a/addons/hr_payroll/tests/test_payslip_flow.py +++ b/addons/hr_payroll/tests/test_payslip_flow.py @@ -74,12 +74,12 @@ class TestPayslipFlow(TestPayslipBase): # I print the payslip report data, format = render_report(self.env.cr, self.env.uid, richard_payslip.ids, 'hr_payroll.report_payslip', {}, {}) if config.get('test_report_directory'): - file(os.path.join(config['test_report_directory'], 'hr_payroll-payslip.'+ format), 'wb+').write(data) + open(os.path.join(config['test_report_directory'], 'hr_payroll-payslip.'+ format), 'wb+').write(data) # I print the payslip details report data, format = render_report(self.env.cr, self.env.uid, richard_payslip.ids, 'hr_payroll.report_payslipdetails', {}, {}) if config.get('test_report_directory'): - file(os.path.join(config['test_report_directory'], 'hr_payroll-payslipdetails.'+ format), 'wb+').write(data) + open(os.path.join(config['test_report_directory'], 'hr_payroll-payslipdetails.'+ format), 'wb+').write(data) # I print the contribution register report context = {'model': 'hr.contribution.register', 'active_ids': [self.ref('hr_payroll.hr_houserent_register')]} diff --git a/addons/hw_escpos/escpos/escpos.py b/addons/hw_escpos/escpos/escpos.py index 1299db9a553..994c7769af6 100644 --- a/addons/hw_escpos/escpos/escpos.py +++ b/addons/hw_escpos/escpos/escpos.py @@ -168,8 +168,7 @@ class StyleStack: def to_escpos(self): """ converts the current style to an escpos command string """ cmd = '' - ordered_cmds = self.cmds.keys() - ordered_cmds.sort(lambda x,y: cmp(self.cmds[x]['_order'], self.cmds[y]['_order'])) + ordered_cmds = sorted(self.cmds.keys(), key=lambda x: self.cmds[x]['_order']) for style in ordered_cmds: cmd += self.cmds[style][self.get(style)] return cmd diff --git a/addons/l10n_ch/models/mail_template.py b/addons/l10n_ch/models/mail_template.py index e67968c5ef7..2ae41812b3e 100644 --- a/addons/l10n_ch/models/mail_template.py +++ b/addons/l10n_ch/models/mail_template.py @@ -4,6 +4,7 @@ import base64 from odoo import api, models +from odoo.tools import pycompat class MailTemplate(models.Model): @@ -20,7 +21,7 @@ class MailTemplate(models.Model): rslt = super(MailTemplate, self).generate_email(res_ids, fields) multi_mode = True - if isinstance(res_ids, (int, long)): + if isinstance(res_ids, pycompat.integer_types): res_ids = [res_ids] multi_mode = False diff --git a/addons/l10n_in_hr_payroll/tests/test_payment_advice.py b/addons/l10n_in_hr_payroll/tests/test_payment_advice.py index 0d418aae4df..d08c0f58b7a 100644 --- a/addons/l10n_in_hr_payroll/tests/test_payment_advice.py +++ b/addons/l10n_in_hr_payroll/tests/test_payment_advice.py @@ -40,4 +40,4 @@ class TestPaymentAdvice(TestPaymentAdviceBase): # In order to test the PDF report defined on a Payment Advice, we will print a Print Advice Report when NEFT is checked data, format = render_report(self.env.cr, self.env.uid, payment_advice.ids, 'l10n_in_hr_payroll.report_payrolladvice', {}, {}) if config.get('test_report_directory'): - file(os.path.join(config['test_report_directory'], 'l10n_in_hr_payroll_summary_report' + format), 'wb+').write(data) + open(os.path.join(config['test_report_directory'], 'l10n_in_hr_payroll_summary_report' + format), 'wb+').write(data) diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index 1fc922a0d55..2c989bb7e2e 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -7,6 +7,8 @@ import copy import datetime import dateutil.relativedelta as relativedelta import logging + +import functools import lxml import urlparse @@ -15,6 +17,7 @@ from urllib import urlencode, quote as quote from odoo import _, api, fields, models, tools from odoo import report as odoo_report from odoo.exceptions import UserError +from odoo.tools import pycompat _logger = logging.getLogger(__name__) @@ -90,10 +93,9 @@ try: 'max': max, 'sum': sum, 'filter': filter, - 'reduce': reduce, + 'reduce': functools.reduce, 'map': map, 'round': round, - 'cmp': cmp, # dateutil.relativedelta is an old-style class and cannot be directly # instanciated wihtin a jinja2 expression, so a lambda "proxy" is @@ -340,7 +342,7 @@ class MailTemplate(models.Model): :param int res_ids: list of ids of document records those mails are related to. """ multi_mode = True - if isinstance(res_ids, (int, long)): + if isinstance(res_ids, pycompat.integer_types): multi_mode = False res_ids = [res_ids] @@ -385,7 +387,7 @@ class MailTemplate(models.Model): @api.multi def get_email_template(self, res_ids): multi_mode = True - if isinstance(res_ids, (int, long)): + if isinstance(res_ids, pycompat.integer_types): res_ids = [res_ids] multi_mode = False @@ -450,7 +452,7 @@ class MailTemplate(models.Model): """ self.ensure_one() multi_mode = True - if isinstance(res_ids, (int, long)): + if isinstance(res_ids, pycompat.integer_types): res_ids = [res_ids] multi_mode = False if fields is None: diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index e7e61b61f3c..e9197a7292d 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -22,6 +22,7 @@ from lxml import etree from werkzeug import url_encode from odoo import _, api, exceptions, fields, models, tools +from odoo.tools import pycompat from odoo.tools.safe_eval import safe_eval @@ -1811,7 +1812,7 @@ class MailThread(models.AbstractModel): partner_ids.add(partner_id[1]) if isinstance(partner_id, (list, tuple)) and partner_id[0] == 6 and len(partner_id) == 3: partner_ids |= set(partner_id[2]) - elif isinstance(partner_id, (int, long)): + elif isinstance(partner_id, pycompat.integer_types): partner_ids.add(partner_id) else: pass # we do not manage anything else diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py index 1f2a30af659..4958c043bb7 100644 --- a/addons/mail/models/res_partner.py +++ b/addons/mail/models/res_partner.py @@ -4,6 +4,8 @@ import logging import threading +from odoo.tools.misc import split_every + from odoo import _, api, fields, models, registry, SUPERUSER_ID from odoo.osv import expression @@ -107,9 +109,8 @@ class Partner(models.Model): @api.model def _notify_send(self, body, subject, recipients, **mail_values): emails = self.env['mail.mail'] - recipients_nbr, recipients_max = len(recipients), 50 - email_chunks = [recipients[x:x + recipients_max] for x in xrange(0, len(recipients), recipients_max)] - for email_chunk in email_chunks: + recipients_nbr = len(recipients) + for email_chunk in split_every(50, recipients.ids): # TDE FIXME: missing message parameter. So we will find mail_message_id # in the mail_values and browse it. It should already be in the # cache so should not impact performances. @@ -117,9 +118,9 @@ class Partner(models.Model): message = self.env['mail.message'].browse(mail_message_id) if mail_message_id else None if message and message.model and message.res_id and message.model in self.env and hasattr(self.env[message.model], 'message_get_recipient_values'): tig = self.env[message.model].browse(message.res_id) - recipient_values = tig.message_get_recipient_values(notif_message=message, recipient_ids=email_chunk.ids) + recipient_values = tig.message_get_recipient_values(notif_message=message, recipient_ids=email_chunk) else: - recipient_values = self.env['mail.thread'].message_get_recipient_values(notif_message=None, recipient_ids=email_chunk.ids) + recipient_values = self.env['mail.thread'].message_get_recipient_values(notif_message=None, recipient_ids=email_chunk) create_values = { 'body_html': body, 'subject': subject, diff --git a/addons/mail/models/res_users.py b/addons/mail/models/res_users.py index a3130c36d44..059e0f521ad 100644 --- a/addons/mail/models/res_users.py +++ b/addons/mail/models/res_users.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import _, api, exceptions, fields, models +from odoo.tools import pycompat class Users(models.Model): @@ -97,7 +98,7 @@ class Users(models.Model): current_pids.append(partner_id[1]) elif isinstance(partner_id, (list, tuple)) and partner_id[0] == 6 and len(partner_id) == 3: current_pids.append(partner_id[2]) - elif isinstance(partner_id, (int, long)): + elif isinstance(partner_id, pycompat.integer_types): current_pids.append(partner_id) if user_pid not in current_pids: partner_ids.append(user_pid) diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 35114a2542b..92822154ff3 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -5,6 +5,7 @@ import base64 import re from odoo import _, api, fields, models, SUPERUSER_ID, tools +from odoo.tools import pycompat from odoo.tools.safe_eval import safe_eval @@ -434,7 +435,7 @@ class MailComposer(models.TransientModel): """ self.ensure_one() multi_mode = True - if isinstance(res_ids, (int, long)): + if isinstance(res_ids, pycompat.integer_types): multi_mode = False res_ids = [res_ids] @@ -484,7 +485,7 @@ class MailComposer(models.TransientModel): """ Call email_template.generate_email(), get fields relevant for mail.compose.message, transform email_cc and email_to into partner_ids """ multi_mode = True - if isinstance(res_ids, (int, long)): + if isinstance(res_ids, pycompat.integer_types): multi_mode = False res_ids = [res_ids] diff --git a/addons/payment_ogone/models/payment.py b/addons/payment_ogone/models/payment.py index 770af5f5f2d..7f5a23927f0 100644 --- a/addons/payment_ogone/models/payment.py +++ b/addons/payment_ogone/models/payment.py @@ -344,7 +344,7 @@ class PaymentTxOgone(models.Model): 'USERID': account.ogone_userid, 'PSWD': account.ogone_password, 'ORDERID': reference, - 'AMOUNT': long(self.amount * 100), + 'AMOUNT': self.amount * 100, 'CURRENCY': self.currency_id.name, 'OPERATION': 'SAL', 'ECI': 2, # Recurring (from MOTO) diff --git a/addons/product/models/product.py b/addons/product/models/product.py index 4b06ea68f9a..9d786281383 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -9,6 +9,8 @@ from odoo.osv import expression import odoo.addons.decimal_precision as dp +from odoo.tools import pycompat + class ProductCategory(models.Model): _name = "product.category" @@ -184,7 +186,7 @@ class ProductProduct(models.Model): pricelist_name_search = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1) if pricelist_name_search: pricelist = self.env['product.pricelist'].browse([pricelist_name_search[0][0]]) - elif isinstance(pricelist_id_or_name, (int, long)): + elif isinstance(pricelist_id_or_name, pycompat.integer_types): pricelist = self.env['product.pricelist'].browse(pricelist_id_or_name) if pricelist: diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 0082bbf7793..2aab1114448 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -8,6 +8,7 @@ import odoo.addons.decimal_precision as dp from odoo import api, fields, models, tools, _ from odoo.exceptions import ValidationError, except_orm +from odoo.tools import pycompat class ProductTemplate(models.Model): @@ -164,7 +165,7 @@ class ProductTemplate(models.Model): # Support context pricelists specified as display_name or ID for compatibility if isinstance(pricelist_id_or_name, basestring): pricelist = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1) - elif isinstance(pricelist_id_or_name, (int, long)): + elif isinstance(pricelist_id_or_name, pycompat.integer_types): pricelist = self.env['product.pricelist'].browse(pricelist_id_or_name) if pricelist: diff --git a/addons/report/models/report.py b/addons/report/models/report.py index 4c72a9d5058..55abb28a638 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -577,7 +577,7 @@ class Report(models.Model): writer = PdfFileWriter() streams = [] # We have to close the streams *after* PdfFilWriter's call to write() for document in documents: - pdfreport = file(document, 'rb') + pdfreport = open(document, 'rb') streams.append(pdfreport) reader = PdfFileReader(pdfreport) for page in range(0, reader.getNumPages()): diff --git a/addons/report_intrastat/tests/test_report_intrastat.py b/addons/report_intrastat/tests/test_report_intrastat.py index fdd2b4d8e1f..90d688f922b 100644 --- a/addons/report_intrastat/tests/test_report_intrastat.py +++ b/addons/report_intrastat/tests/test_report_intrastat.py @@ -31,4 +31,4 @@ class RepoortIntrastatTest(common.TransactionCase): def test_00_create_pdf(self): data, report_format = self.env['ir.actions.report.xml'].render_report(self.invoice.ids, 'report_intrastat.report_intrastatinvoice', {}) if tools.config['test_report_directory']: - file(os.path.join(tools.config['test_report_directory'], 'report_intrastat-intrastat_report.' + report_format), 'wb+').write(data) + open(os.path.join(tools.config['test_report_directory'], 'report_intrastat-intrastat_report.' + report_format), 'wb+').write(data) diff --git a/addons/resource/tests/test_resource.py b/addons/resource/tests/test_resource.py index 3a545f5feef..37e1d46fee7 100644 --- a/addons/resource/tests/test_resource.py +++ b/addons/resource/tests/test_resource.py @@ -4,6 +4,8 @@ import babel.dates from datetime import datetime, timedelta, date, time + +from dateutil import rrule from dateutil.relativedelta import relativedelta from odoo.fields import Date, Datetime @@ -349,13 +351,7 @@ class TestWorkDays(TestResourceCommon): ] }) - self._days = [ - date.fromordinal(o) - for o in xrange( - WAR_START.toordinal(), - WAR_END.toordinal() + 1 - ) - ] + self._days = [dt.date() for dt in rrule.rrule(rrule.DAILY, dtstart=WAR_START, until=WAR_END)] def test_trivial_calendar_no_leaves(self): """ If leaves are not involved, only calendar attendances (basic diff --git a/addons/sale/models/account_invoice.py b/addons/sale/models/account_invoice.py index 713308aa7a1..3d61900129d 100644 --- a/addons/sale/models/account_invoice.py +++ b/addons/sale/models/account_invoice.py @@ -56,11 +56,11 @@ class AccountInvoice(models.Model): def _refund_cleanup_lines(self, lines): result = super(AccountInvoice, self)._refund_cleanup_lines(lines) if self.env.context.get('mode') == 'modify': - for i in xrange(0, len(lines)): - for name, field in lines[i]._fields.iteritems(): + for i, line in enumerate(lines): + for name, field in line._fields.iteritems(): if name == 'sale_line_ids': - result[i][2][name] = [(6, 0, lines[i][name].ids)] - lines[i][name] = False + result[i][2][name] = [(6, 0, line[name].ids)] + line[name] = False return result @api.multi diff --git a/addons/stock/models/procurement.py b/addons/stock/models/procurement.py index b3edca53c0f..7ce3bb2abed 100644 --- a/addons/stock/models/procurement.py +++ b/addons/stock/models/procurement.py @@ -4,6 +4,7 @@ from collections import defaultdict from datetime import datetime from dateutil.relativedelta import relativedelta +from odoo.tools.misc import split_every from psycopg2 import OperationalError from odoo import api, fields, models, registry, _ @@ -239,9 +240,9 @@ class ProcurementOrder(models.Model): # Search all confirmed stock_moves and try to assign them confirmed_moves = self.env['stock.move'].search([('state', '=', 'confirmed')], limit=None, order='priority desc, date_expected asc') - for x in xrange(0, len(confirmed_moves.ids), 100): + for moves_chunk in split_every(100, confirmed_moves.ids): # TDE CLEANME: muf muf - self.env['stock.move'].browse(confirmed_moves.ids[x:x + 100]).action_assign() + self.env['stock.move'].browse(moves_chunk).action_assign() if use_new_cursor: self._cr.commit() if use_new_cursor: diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index a661169296f..63a9be564aa 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -4,6 +4,7 @@ from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError +from odoo.tools import pycompat from odoo.tools.float_utils import float_round from datetime import datetime import operator as py_operator @@ -180,7 +181,7 @@ class Product(models.Model): location_ids = [] if self.env.context.get('location', False): - if isinstance(self.env.context['location'], (int, long)): + if isinstance(self.env.context['location'], pycompat.integer_types): location_ids = [self.env.context['location']] elif isinstance(self.env.context['location'], basestring): domain = [('complete_name', 'ilike', self.env.context['location'])] @@ -191,7 +192,7 @@ class Product(models.Model): location_ids = self.env.context['location'] else: if self.env.context.get('warehouse', False): - if isinstance(self.env.context['warehouse'], (int, long)): + if isinstance(self.env.context['warehouse'], pycompat.integer_types): wids = [self.env.context['warehouse']] elif isinstance(self.env.context['warehouse'], basestring): domain = [('name', 'ilike', self.env.context['warehouse'])] @@ -326,7 +327,7 @@ class Product(models.Model): @api.model def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): res = super(Product, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) - if self._context.get('location') and isinstance(self._context['location'], (int, long)): + if self._context.get('location') and isinstance(self._context['location'], pycompat.integer_types): location = self.env['stock.location'].browse(self._context['location']) fields = res.get('fields') if fields: diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py index 2dc9cba0fdb..65464fa7493 100644 --- a/addons/web_editor/models/ir_ui_view.py +++ b/addons/web_editor/models/ir_ui_view.py @@ -7,6 +7,7 @@ from lxml import etree, html from odoo.exceptions import AccessError from odoo import api, models +from odoo.tools import pycompat _logger = logging.getLogger(__name__) @@ -121,7 +122,7 @@ class IrUiView(models.Model): def _view_obj(self, view_id): if isinstance(view_id, basestring): return self.env.ref(view_id) - elif isinstance(view_id, (int, long)): + elif isinstance(view_id, pycompat.integer_types): return self.browse(view_id) # assume it's already a view object (WTF?) return view_id diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index ce93898914e..42f90e0cc0c 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -10,6 +10,7 @@ from odoo import tools from odoo.addons.website.models import website from odoo.http import request +from odoo.tools import pycompat _logger = logging.getLogger(__name__) @@ -63,7 +64,7 @@ class View(models.Model): return views.filter_duplicate() else: return self.env.ref(view_id) - elif isinstance(view_id, (int, long)): + elif isinstance(view_id, pycompat.integer_types): return self.browse(view_id) # assume it's already a view object (WTF?) @@ -72,7 +73,7 @@ class View(models.Model): @api.model @tools.ormcache_context('self._uid', 'xml_id', keys=('website_id',)) def get_view_id(self, xml_id): - if 'website_id' in self._context and not isinstance(xml_id, (int, long)): + if 'website_id' in self._context and not isinstance(xml_id, pycompat.integer_types): domain = [('key', '=', xml_id), '|', ('website_id', '=', self._context['website_id']), ('website_id', '=', False)] view = self.search(domain, order='website_id', limit=1) if not view: diff --git a/addons/website/models/website.py b/addons/website/models/website.py index bb26fc7ee85..bd7621c767d 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -19,7 +19,7 @@ except ImportError: from odoo import api, fields, models from odoo import tools -from odoo.tools import ustr +from odoo.tools import ustr, pycompat from odoo.http import request from odoo.tools.translate import _ @@ -421,7 +421,7 @@ class Website(models.Model): @api.model def get_template(self, template): View = self.env['ir.ui.view'] - if isinstance(template, (int, long)): + if isinstance(template, pycompat.integer_types): view_id = template else: if '.' not in template: @@ -486,7 +486,7 @@ class Website(models.Model): 'num': pmax }, "pages": [ - {'url': get_url(page), 'num': page} for page in xrange(pmin, pmax+1) + {'url': get_url(page), 'num': page} for page in pycompat.range(pmin, pmax+1) ] } @@ -544,8 +544,7 @@ class Website(models.Model): values = [{}] convitems = converters.items() # converters with a domain are processed after the other ones - gd = lambda x: hasattr(x[1], 'domain') and (x[1].domain != '[]') - convitems.sort(lambda x, y: cmp(gd(x), gd(y))) + convitems.sort(key=lambda x: hasattr(x[1], 'domain') and (x[1].domain != '[]')) for (i, (name, converter)) in enumerate(convitems): newval = [] for val in values: diff --git a/addons/website/tests/test_crawl.py b/addons/website/tests/test_crawl.py index 5ef5c551163..704cd62a41b 100644 --- a/addons/website/tests/test_crawl.py +++ b/addons/website/tests/test_crawl.py @@ -10,6 +10,8 @@ import lxml.html import odoo import re +from odoo.tools import pycompat + _logger = logging.getLogger(__name__) @@ -38,7 +40,7 @@ class Crawler(odoo.tests.HttpCase): _logger.info("%s %s", msg, url) r = self.url_open(url) code = r.getcode() - self.assertIn(code, xrange(200, 300), "%s Fetching %s returned error response (%d)" % (msg, url, code)) + self.assertIn(code, pycompat.range(200, 300), "%s Fetching %s returned error response (%d)" % (msg, url, code)) if r.info().gettype() == 'text/html': doc = lxml.html.fromstring(r.read()) diff --git a/doc/python3.rst b/doc/python3.rst index cb60965eb6c..407d8e9ee56 100644 --- a/doc/python3.rst +++ b/doc/python3.rst @@ -16,13 +16,28 @@ with both. Here are a few useful steps or reminders to make Python 2 code compatible with Python 3. +.. important:: + + This is not a general-purpose guide for porting Python 2 to Python 3, it's + a guide to write 2/3-compatible Odoo code. It does not go through all the + changes in Python but rather through issues which have been found in the + standard Odoo distribution in order to show how to evolve such code such + that it works on both Python 2 and Python 3. + References/useful documents: +* `What's new in Python 3? `_ + covers many of the changes between Python 2 and Python 3, though it is + missing a number of changes which `were backported to Python 2.7 `_ + as well as :ref:`some feature reintroductions ` of later Python 3 + revisions * `How do I port to Python 3? `_ * `Python-Future `_ * `Porting Python 2 code to Python 3 `_ * `Porting to Python 3: A Guide `_ (a bit outdated but useful for the extensive comments on strings and IO) +.. _p3support: + Versions Support ================ @@ -37,16 +52,17 @@ features whereas: * Python 3.4 has no specific compatibility improvement but is the lowest P3 version for PyLint * Python 3.3 reintroduced the "u" prefix for proper (unicode) strings -* Python 3.2 made ``range`` views more list-like and reintroduced ``callable`` +* Python 3.2 made ``range`` views more list-like (backported to 2.7)and + reintroduced ``callable`` .. warning:: - Python 3 adds plenty of great features (keyword-only parameters, - generator delegation, pathlib, ...), do not use them until Python 2 - support is dropped + While Python 3 adds plenty of great features (keyword-only parameters, + generator delegation, pathlib, ...), you must *not *use them in Odoo + until Python 2 support is dropped -Fixes -===== +Moved and removed +================= Exception Handlers ------------------ @@ -87,33 +103,132 @@ or:: This form was implemented in Python 2.5 and is thus compatible across the board. -Removed Operators ------------------ +Operators & keywords +-------------------- -.. important:: +.. important:: The backtick operator ```foo``` must be converted to an + explicit call to the ``repr()`` builtin - * The backtick operator ``\`foo\``` must be converted to an explicit call - to the ``repr()`` builtin - * The ``<>`` operator must be replaced by ``!=`` +.. important:: The ``<>`` operator must be replaced by ``!=`` These two operators were long recommended against/deprecated in Python 2, Python 3 removed them from the language. -Removed/renamed builtins ------------------------- +.. _changed-exec: + +.. important:: ``exec`` is now a builtin + +In Python 2, ``exec`` is a statement/keyword. Much like ``print``, it's been +converted to a builtin function in Python 3. However because the Python 2 +version can take a tuple parameter it is easy to convert the odd ``exec`` +statement to the following cross-language forms:: + + exec(source) + exec(source, globals) + exec(source, globals, locals) + +builtins +-------- + +``cmp`` +####### + +The ``cmp`` builtin function has been removed from Python 3. + +* Most of its uses are in ``cmp=`` parameters to sort functions where it can + usually be replaced by a key function. +* Other uses found were obtaining the sign of an item (``cmp(item, 0)``), this + can be replicated using the standard library's ``math.copysign`` e.g. + ``math.copysign(1, item)`` will return ``1.0`` if ``item`` is positive and + ``-1.0`` if ``item`` is negative. + +``execfile`` +############ + +``execfile(path)`` has been removed completely from Python 3 but it is +trivially replaceable in all cases by:: + + exec(open(path, 'rb').open()) + +of a variant thereof (see :ref:`exec changes ` for details) + +``file`` +######## + +The ``file`` builtin has been removed in Python 3. Generally, it can just +be replaced by the ``open`` builtin, although you may want to use ``io.open`` +which is more flexible and better handles the binary/text dichotomy, +:ref:`a big issue in cross-version Python `. + +.. note:: + + In Python 3, the ``open`` builtin is actually an alias for ``io.open``. + +``long`` +######## + +In Python 2, integers can be either ``int`` or ``long``. Python 3 unifies this +under the single ``int`` type. + +.. important:: + + * the ``L`` suffix for integer literals must be removed + * calls to ``long`` must be replaced by calls to ``int`` + * ``(int, long)`` for type-checking purposes must be replaced by + :py:data:`odoo.tools.pycompat.integer_types` + + +* the ``L`` suffix on numbers is unsupported in Python 3, and unnecessary in + Python 2 as "overflowing" integer literals will implicitly instantiate long. +* in Python 2, a call to ``int()`` will implicitly create a ``long`` object if + necessary. +* type-testing is the last and bigger issue as in Python 2 ``long`` is not a + subtype of ``int`` (nor the reverse), and ``isinstance(value, (int, long))`` + is thus generally necessary to catch all integrals. + + For that case, Odoo 11 now provides a compatibility module with an + :py:data:`~odoo.tools.pycompat.integer_types` definition which can be used + for type-testing. + + It is a tuple of types so when used with ``isinstance`` it can be provided + directly or inside an other tuple alongside other types e.g. + ``isinstance(value, (BaseModel, integer_types))``. + + However when used with ``type`` directly (which should be avoided) you + should use the ``in`` operator, and if you need other types you need to + concatenate ``integer_types`` to an other tuple. ``reduce`` ########## In Python 3, ``reduce`` has been demoted from builtin to ``functools.reduce``. -However this is because many (if not most) uses of ``reduce`` can be replaced -by ``sum``, ``all``, ``any`` or a list comprehension for a more readable and -faster result. +However this is because *most uses of ``reduce`` can be replaced by ``sum``, +``all``, ``any``* or a list comprehension for a more readable and faster +result. It is easy enough to just add ``from functools import reduce`` to the file and compatible with Python 2.6 and later, but consider whether you get better code by replacing it with some other method altogether. +``xrange`` +########## + +In Python 3, ``range()`` behaves the same as Python 3's ``xrange``. For +cross-versions code you can: + +* just use ``range()`` everywhere and ignore the allocation cost of a list in + Python 2 (often not an issue) +* conditionally alias ``xrange`` to ``range`` in Python 3 and use that +* use a combination of ``itertools.count`` and ``takewhile`` for a + cross-compatible lazy increasing sequence of numbers + +.. warning:: + + In the *rare* cases where you need conditional code (code which applies + for one version of python and not the other), use ``sys.version_info`` + e.g. ``sys.version_info() >= (3,)`` for Python3+ code or + ``sys.version_info() < (3,)`` for Python 2 code. + Removed/renamed methods ----------------------- diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py index 4c64a172057..79118c845f2 100644 --- a/odoo/addons/base/ir/ir_model.py +++ b/odoo/addons/base/ir/ir_model.py @@ -10,6 +10,7 @@ from odoo import api, fields, models, SUPERUSER_ID, tools, _ from odoo.exceptions import AccessError, UserError, ValidationError from odoo.modules.registry import Registry from odoo.osv import expression +from odoo.tools import pycompat from odoo.tools.safe_eval import safe_eval _logger = logging.getLogger(__name__) @@ -1055,7 +1056,7 @@ class IrModelAccess(models.Model): else: model_name = model - if isinstance(group_ids, (int, long)): + if isinstance(group_ids, pycompat.integer_types): group_ids = [group_ids] query = """ SELECT 1 FROM ir_model_access a diff --git a/odoo/addons/base/ir/ir_qweb/ir_qweb.py b/odoo/addons/base/ir/ir_qweb/ir_qweb.py index dc71f3304ab..20a640be8d4 100644 --- a/odoo/addons/base/ir/ir_qweb/ir_qweb.py +++ b/odoo/addons/base/ir/ir_qweb/ir_qweb.py @@ -4,6 +4,7 @@ import ast from urlparse import urlparse from lxml import html +from odoo.tools import pycompat from .qweb import QWeb, Contextifier from .assetsbundle import AssetsBundle from lxml import etree @@ -90,7 +91,7 @@ class IrQWeb(models.AbstractModel, QWeb): view = self.env['ir.ui.view'].browse(view_id) return view.inherit_id is not None - if isinstance(name, (int, long)) or is_child_view(name): + if isinstance(name, pycompat.integer_types) or is_child_view(name): for node in etree.fromstring(template): if node.get('t-name'): node.set('t-name', str(name)) diff --git a/odoo/addons/base/ir/ir_qweb/qweb.py b/odoo/addons/base/ir/ir_qweb/qweb.py index 0c69bd90baa..a4204326344 100644 --- a/odoo/addons/base/ir/ir_qweb/qweb.py +++ b/odoo/addons/base/ir/ir_qweb/qweb.py @@ -11,6 +11,9 @@ from werkzeug.utils import escape as _escape from itertools import chain, izip, tee import __builtin__ from functools import reduce + +from odoo.tools import pycompat + builtin_defaults = {name: getattr(__builtin__, name) for name in dir(__builtin__)} try: @@ -160,7 +163,7 @@ def foreach_iterator(base_ctx, enum, name): if not enum: return if isinstance(enum, int): - enum = xrange(enum) + enum = pycompat.range(enum) size = None if isinstance(enum, Sized): ctx["%s_size" % name] = size = len(enum) diff --git a/odoo/addons/base/ir/ir_translation.py b/odoo/addons/base/ir/ir_translation.py index bf580bbf3bf..7d73f40d65b 100644 --- a/odoo/addons/base/ir/ir_translation.py +++ b/odoo/addons/base/ir/ir_translation.py @@ -8,6 +8,7 @@ from difflib import get_close_matches from odoo import api, fields, models, tools, SUPERUSER_ID, _ from odoo.exceptions import AccessError, UserError, ValidationError from odoo.modules import get_module_path, get_module_resource +from odoo.tools import pycompat _logger = logging.getLogger(__name__) @@ -395,7 +396,7 @@ class IrTranslation(models.Model): if isinstance(types, basestring): types = (types,) if res_id: - if isinstance(res_id, (int, long)): + if isinstance(res_id, pycompat.integer_types): res_id = (res_id,) else: res_id = tuple(res_id) diff --git a/odoo/addons/base/ir/ir_ui_menu.py b/odoo/addons/base/ir/ir_ui_menu.py index 15ba7509700..c8a39322835 100644 --- a/odoo/addons/base/ir/ir_ui_menu.py +++ b/odoo/addons/base/ir/ir_ui_menu.py @@ -133,9 +133,9 @@ class IrUiMenu(models.Model): if not self._context.get('ir.ui.menu.full_list'): menus = menus._filter_visible_menus() if offset: - menus = menus[long(offset):] + menus = menus[offset:] if limit: - menus = menus[:long(limit)] + menus = menus[:limit] return len(menus) if count else menus @api.multi diff --git a/odoo/addons/base/ir/ir_ui_view.py b/odoo/addons/base/ir/ir_ui_view.py index 729d4a8364d..c557d15ee6b 100644 --- a/odoo/addons/base/ir/ir_ui_view.py +++ b/odoo/addons/base/ir/ir_ui_view.py @@ -22,7 +22,7 @@ from odoo.exceptions import ValidationError from odoo.http import request from odoo.modules.module import get_resource_from_path, get_resource_path from odoo.osv import orm -from odoo.tools import config, graph, ConstantMapping, SKIPPED_ELEMENT_TYPES +from odoo.tools import config, graph, ConstantMapping, SKIPPED_ELEMENT_TYPES, pycompat from odoo.tools.convert import _fix_multiple_roots from odoo.tools.parse_version import parse_version from odoo.tools.safe_eval import safe_eval @@ -944,7 +944,7 @@ actual arch. view ID or an XML ID. Note that this method may be overridden for other kinds of template values. """ - if isinstance(template, (int, long)): + if isinstance(template, pycompat.integer_types): return template if '.' not in template: raise ValueError('Invalid template id: %r' % template) @@ -1045,7 +1045,7 @@ actual arch. @api.multi def render(self, values=None, engine='ir.qweb'): - assert isinstance(self.id, (int, long)) + assert isinstance(self.id, pycompat.integer_types) qcontext = dict( env=self.env, diff --git a/odoo/addons/base/res/ir_property.py b/odoo/addons/base/res/ir_property.py index 4dcc33209f2..8e1b7eda860 100644 --- a/odoo/addons/base/res/ir_property.py +++ b/odoo/addons/base/res/ir_property.py @@ -5,6 +5,7 @@ from operator import itemgetter from odoo import api, fields, models, _ from odoo.exceptions import UserError +from odoo.tools import pycompat TYPE2FIELD = { 'char': 'value_text', @@ -70,7 +71,7 @@ class Property(models.Model): if field == 'value_reference': if isinstance(value, models.BaseModel): value = '%s,%d' % (value._name, value.id) - elif isinstance(value, (int, long)): + elif isinstance(value, pycompat.integer_types): field_id = values.get('fields_id') if not field_id: if not prop: diff --git a/odoo/addons/base/tests/test_api.py b/odoo/addons/base/tests/test_api.py index cbb63daec59..8270d6086d5 100644 --- a/odoo/addons/base/tests/test_api.py +++ b/odoo/addons/base/tests/test_api.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import models -from odoo.tools import mute_logger +from odoo.tools import mute_logger, pycompat from odoo.tests import common from odoo.exceptions import AccessError @@ -69,8 +69,8 @@ class TestAPI(common.TransactionCase): self.cr.execute("SELECT COUNT(*) FROM res_partner WHERE active") count1 = self.cr.fetchone()[0] count2 = self.env['res.partner'].search([], count=True) - self.assertIsInstance(count1, (int, long)) - self.assertIsInstance(count2, (int, long)) + self.assertIsInstance(count1, pycompat.integer_types) + self.assertIsInstance(count2, pycompat.integer_types) self.assertEqual(count1, count2) @mute_logger('odoo.models') diff --git a/odoo/addons/base/tests/test_expression.py b/odoo/addons/base/tests/test_expression.py index 104c4a6dfe1..4dea4797b0c 100644 --- a/odoo/addons/base/tests/test_expression.py +++ b/odoo/addons/base/tests/test_expression.py @@ -214,9 +214,8 @@ class TestExpression(TransactionCase): # create new company with partners, and partners with no company company2 = self.env['res.company'].create({'name': 'Acme 2'}) - for i in xrange(4): + for i in range(4): Partner.create({'name': 'P of Acme %s' % i, 'company_id': company2.id}) - for i in xrange(4): Partner.create({'name': 'P of All %s' % i, 'company_id': False}) # check if many2one works with negative empty list diff --git a/odoo/addons/base/tests/test_float.py b/odoo/addons/base/tests/test_float.py index 03e15b5877e..8c4ee380bd2 100644 --- a/odoo/addons/base/tests/test_float.py +++ b/odoo/addons/base/tests/test_float.py @@ -4,7 +4,7 @@ from math import log10 from odoo.tests.common import TransactionCase -from odoo.tools import float_compare, float_is_zero, float_repr, float_round, float_split_str +from odoo.tools import float_compare, float_is_zero, float_repr, float_round, float_split_str, pycompat class TestFloatPrecision(TransactionCase): @@ -98,14 +98,13 @@ class TestFloatPrecision(TransactionCase): precisions = [2, 2, 2, 2, 2, 2, 3, 4] # Note: max precision for double floats is 53 bits of precision or # 17 significant decimal digits - for magnitude in range(7): - for i in xrange(len(fractions)): - frac, exp, prec = fractions[i], expecteds[i], precisions[i] + for magnitude in pycompat.range(7): + for frac, exp, prec in zip(fractions, expecteds, precisions): for sign in [-1,1]: - for x in xrange(0,10000,97): - n = x * 10**magnitude + for x in pycompat.range(0, 10000, 97): + n = x * 10 ** magnitude f = sign * (n + frac) - f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp + f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp try_round(f, f_exp, digits=prec) def try_zero(amount, expected): diff --git a/odoo/addons/base/tests/test_ir_sequence.py b/odoo/addons/base/tests/test_ir_sequence.py index 76db66d7008..946043d9f4b 100644 --- a/odoo/addons/base/tests/test_ir_sequence.py +++ b/odoo/addons/base/tests/test_ir_sequence.py @@ -154,7 +154,7 @@ class TestIrSequenceGenerate(unittest.TestCase): self.assertTrue(seq) with environment() as env: - for i in xrange(1, 10): + for i in range(1, 10): n = env['ir.sequence'].next_by_code('test_sequence_type_5') self.assertEqual(n, str(i)) @@ -169,7 +169,7 @@ class TestIrSequenceGenerate(unittest.TestCase): self.assertTrue(seq) with environment() as env: - for i in xrange(1, 10): + for i in range(1, 10): n = env['ir.sequence'].next_by_code('test_sequence_type_6') self.assertEqual(n, str(i)) diff --git a/odoo/addons/base/tests/test_ir_sequence_date_range.py b/odoo/addons/base/tests/test_ir_sequence_date_range.py index 147b0f26050..7bd43eb4ea9 100644 --- a/odoo/addons/base/tests/test_ir_sequence_date_range.py +++ b/odoo/addons/base/tests/test_ir_sequence_date_range.py @@ -116,16 +116,16 @@ class TestIrSequenceDateRangeChangeImplementation(SingleTransactionCase): seq = self.env['ir.sequence'] seq16 = self.env['ir.sequence'].with_context({'ir_sequence_date': january(16)}) - for i in xrange(1, 5): + for i in range(1, 5): n = seq.next_by_code('test_sequence_date_range_3') self.assertEqual(n, str(i)) - for i in xrange(1, 5): + for i in range(1, 5): n = seq16.next_by_code('test_sequence_date_range_3') self.assertEqual(n, str(i)) - for i in xrange(1, 5): + for i in range(1, 5): n = seq.next_by_code('test_sequence_date_range_4') self.assertEqual(n, str(i)) - for i in xrange(1, 5): + for i in range(1, 5): n = seq16.next_by_code('test_sequence_date_range_4') self.assertEqual(n, str(i)) diff --git a/odoo/addons/base/tests/test_misc.py b/odoo/addons/base/tests/test_misc.py index b8ede104a7c..c148b901982 100644 --- a/odoo/addons/base/tests/test_misc.py +++ b/odoo/addons/base/tests/test_misc.py @@ -16,14 +16,14 @@ class TestCountingStream(unittest.TestCase): self.assertEqual(s.index, 0) def test_single(self): - s = misc.CountingStream(xrange(1)) + s = misc.CountingStream(range(1)) self.assertEqual(s.index, -1) self.assertEqual(next(s, None), 0) self.assertIsNone(next(s, None)) self.assertEqual(s.index, 1) def test_full(self): - s = misc.CountingStream(xrange(42)) + s = misc.CountingStream(range(42)) for _ in s: pass self.assertEqual(s.index, 42) diff --git a/odoo/addons/base/tests/test_orm.py b/odoo/addons/base/tests/test_orm.py index ef586218a0d..0da71a84ad5 100644 --- a/odoo/addons/base/tests/test_orm.py +++ b/odoo/addons/base/tests/test_orm.py @@ -5,7 +5,7 @@ from collections import defaultdict from odoo.exceptions import AccessError, MissingError from odoo.tests.common import TransactionCase -from odoo.tools import mute_logger +from odoo.tools import mute_logger, pycompat class TestORM(TransactionCase): @@ -193,7 +193,7 @@ class TestInherits(TransactionCase): """ `default_get` cannot return a dictionary or a new id """ defaults = self.env['res.users'].default_get(['partner_id']) if 'partner_id' in defaults: - self.assertIsInstance(defaults['partner_id'], (bool, int, long)) + self.assertIsInstance(defaults['partner_id'], (bool, pycompat.integer_types)) def test_create(self): """ creating a user should automatically create a new partner """ diff --git a/odoo/addons/base/tests/test_res_config.py b/odoo/addons/base/tests/test_res_config.py index af919ce4b66..89300bf43a5 100644 --- a/odoo/addons/base/tests/test_res_config.py +++ b/odoo/addons/base/tests/test_res_config.py @@ -3,6 +3,7 @@ from odoo import exceptions from odoo.tests.common import TransactionCase +from odoo.tools import pycompat class TestResConfig(TransactionCase): @@ -42,7 +43,7 @@ class TestResConfig(TransactionCase): self.assertIsInstance(res, tuple) self.assertEqual(len(res), 2, "The result should contain 2 elements") self.assertIsInstance(res[0], basestring) - self.assertIsInstance(res[1], (int, long)) + self.assertIsInstance(res[1], pycompat.integer_types) # Check returned values self.assertEqual(res[0], self.expected_path) diff --git a/odoo/addons/test_limits/models.py b/odoo/addons/test_limits/models.py index f46402857cf..506578a811b 100644 --- a/odoo/addons/test_limits/models.py +++ b/odoo/addons/test_limits/models.py @@ -2,8 +2,10 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import time +import sys from odoo import models, api +from odoo.tools import pycompat class m(models.Model): """ This model exposes a few methods that will consume between 'almost no @@ -37,7 +39,7 @@ class m(models.Model): t0 = time.clock() t1 = time.clock() while t1 - t0 < seconds: - for i in xrange(10000000): + for i in pycompat.range(10000000): x = i * i t1 = time.clock() return True diff --git a/odoo/addons/test_new_api/tests/test_attributes.py b/odoo/addons/test_new_api/tests/test_attributes.py index 636dadf2a06..b37afe7e30d 100644 --- a/odoo/addons/test_new_api/tests/test_attributes.py +++ b/odoo/addons/test_new_api/tests/test_attributes.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from odoo.tests import common +from odoo.tools import pycompat ANSWER_TO_ULTIMATE_QUESTION = 42 @@ -16,7 +17,7 @@ class TestAttributes(common.TransactionCase): self.assertTrue(hasattr(instance, 'unknown')) # Is it the right type ? - self.assertIsInstance(instance.unknown, (int, long)) + self.assertIsInstance(instance.unknown, pycompat.integer_types) # Is it the right value, in case of, we don't know ;-) self.assertEqual(instance.unknown, ANSWER_TO_ULTIMATE_QUESTION) diff --git a/odoo/addons/test_pylint/tests/test_pylint.py b/odoo/addons/test_pylint/tests/test_pylint.py index 38fa4c3d51a..02c1a14a261 100644 --- a/odoo/addons/test_pylint/tests/test_pylint.py +++ b/odoo/addons/test_pylint/tests/test_pylint.py @@ -25,6 +25,21 @@ class TestPyLint(TransactionCase): 'E0601', # using variable before assignment 'W0123', # eval used 'W0101', # unreachable code + + 'long-suffix', + 'apply-builtin', + 'cmp-builtin', + 'coerce-builtin', + 'execfile-builtin', + 'input-builtin', + 'intern-builtin', + 'long-builtin', + 'raw_input-builtin', + 'reload-builtin', + 'xrange-builtin', + # TODO: enable once report has been removed + # 'file-builtin', + # 'reduce-builtin', ] def _skip_test(self, reason): diff --git a/odoo/fields.py b/odoo/fields.py index 2b289227f9f..d713167a101 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -15,8 +15,7 @@ import xmlrpclib import psycopg2 from odoo.sql_db import LazyCursor -from odoo.tools import float_precision, float_repr, float_round, frozendict, \ - html_sanitize, human_size, pg_varchar, ustr, OrderedSet +from odoo.tools import float_precision, float_repr, float_round, frozendict, html_sanitize, human_size, pg_varchar, ustr, OrderedSet, pycompat from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT from odoo.tools.translate import html_translate, _ @@ -1656,7 +1655,7 @@ class Binary(Field): def convert_to_cache(self, value, record, validate=True): if isinstance(value, buffer): return str(value) - if isinstance(value, (int, long)) and \ + if isinstance(value, pycompat.integer_types) and \ (record._context.get('bin_size') or record._context.get('bin_size_' + self.name)): # If the client requests only the size of the field, we return that diff --git a/odoo/http.py b/odoo/http.py index a8dbf098576..9eb9e75e101 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -621,10 +621,10 @@ class JsonRequest(WebRequest): # We need then to manage http sessions manually. response['session_id'] = self.session.sid mime = 'application/javascript' - body = "%s(%s);" % (self.jsonp, json.dumps(response),) + body = "%s(%s);" % (self.jsonp, json.dumps(response, default=ustr),) else: mime = 'application/json' - body = json.dumps(response) + body = json.dumps(response, default=ustr) return Response( body, headers=[('Content-Type', mime), @@ -695,7 +695,7 @@ def serialize_exception(e): "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__, "debug": traceback.format_exc(), "message": ustr(e), - "arguments": to_jsonable(e.args), + "arguments": e.args, "exception_type": "internal_error" } if isinstance(e, odoo.exceptions.UserError): @@ -716,19 +716,6 @@ def serialize_exception(e): tmp["exception_type"] = "except_orm" return tmp -def to_jsonable(o): - if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \ - or isinstance(o, bool) or o is None or isinstance(o, float): - return o - if isinstance(o, list) or isinstance(o, tuple): - return [to_jsonable(x) for x in o] - if isinstance(o, dict): - tmp = {} - for k, v in o.items(): - tmp[u"%s" % k] = to_jsonable(v) - return tmp - return ustr(o) - class HttpRequest(WebRequest): """ Handler for the ``http`` request type. diff --git a/odoo/models.py b/odoo/models.py index fe302fb0a4f..e5e56b7ff76 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -41,6 +41,7 @@ from lxml import etree from lxml.builder import E import odoo +from odoo.tools import pycompat from . import SUPERUSER_ID from . import api from . import tools @@ -173,7 +174,7 @@ class NewId(object): def __nonzero__(self): return False -IdType = (int, long, str, unicode, NewId) +IdType = pycompat.integer_types + (str, unicode, NewId) # maximum number of prefetched records @@ -1391,7 +1392,7 @@ class BaseModel(object): provided domain `. """ res = self.search(args, count=True) - return res if isinstance(res, (int, long)) else len(res) + return res if isinstance(res, pycompat.integer_types) else len(res) @api.model @api.returns('self', @@ -1536,7 +1537,7 @@ class BaseModel(object): # override defaults with the provided values, never allow the other way around defaults = self.default_get(list(missing_defaults)) for name, value in defaults.iteritems(): - if self._fields[name].type == 'many2many' and value and isinstance(value[0], (int, long)): + if self._fields[name].type == 'many2many' and value and isinstance(value[0], pycompat.integer_types): # convert a list of ids into a list of commands defaults[name] = [(6, 0, value)] elif self._fields[name].type == 'one2many' and value and isinstance(value[0], dict): @@ -3862,7 +3863,7 @@ class BaseModel(object): """ ids, new_ids = [], [] for i in self._ids: - (ids if isinstance(i, (int, long)) else new_ids).append(i) + (ids if isinstance(i, pycompat.integer_types) else new_ids).append(i) if not ids: return self query = """SELECT id FROM "%s" WHERE id IN %%s""" % self._table diff --git a/odoo/modules/graph.py b/odoo/modules/graph.py index ca4998fd254..9a11949c508 100644 --- a/odoo/modules/graph.py +++ b/odoo/modules/graph.py @@ -165,7 +165,7 @@ class Node(object): 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)) + self.children.sort(key=lambda x: x.name) return node def __setattr__(self, name, value): diff --git a/odoo/modules/migration.py b/odoo/modules/migration.py index 552c9d8dae7..5b1dcf09ab2 100644 --- a/odoo/modules/migration.py +++ b/odoo/modules/migration.py @@ -137,7 +137,8 @@ class MigrationManager(object): try: fp, fname = tools.file_open(pyfile, pathinfo=True) - if not isinstance(fp, file): + # FIXME: imp.load_source removed in P3, and so is the ``file`` object... + if not isinstance(fp, file):# pylint: disable=file-builtin # 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() diff --git a/odoo/modules/module.py b/odoo/modules/module.py index 398585ba239..dd1485e1d8a 100644 --- a/odoo/modules/module.py +++ b/odoo/modules/module.py @@ -78,7 +78,7 @@ class AddonsHook(object): # execute source in context of module *after* putting everything in # sys.modules, so recursive import works - execfile(modfile, new_mod.__dict__) + exec(open(modfile, 'rb').read(), new_mod.__dict__) # people import openerp.addons and expect openerp.addons. to work setattr(odoo.addons, addon_name, new_mod) diff --git a/odoo/osv/expression.py b/odoo/osv/expression.py index bef1cb1c523..b98fca44b39 100644 --- a/odoo/osv/expression.py +++ b/odoo/osv/expression.py @@ -121,6 +121,7 @@ from functools import partial from zlib import crc32 import odoo.modules +from odoo.tools import pycompat from ..models import MAGIC_COLUMNS, BaseModel import odoo.tools as tools @@ -700,7 +701,7 @@ class expression(object): names = [value] elif value and isinstance(value, (tuple, list)) and all(isinstance(item, basestring) for item in value): names = value - elif isinstance(value, (int, long)): + elif isinstance(value, pycompat.integer_types): return [value] if names: return list({ diff --git a/odoo/service/model.py b/odoo/service/model.py index 6502f74d2bb..33a7fb2b93f 100644 --- a/odoo/service/model.py +++ b/odoo/service/model.py @@ -10,6 +10,7 @@ import time import odoo from odoo.exceptions import UserError, ValidationError, QWebException from odoo.models import check_method_name +from odoo.tools import pycompat from odoo.tools.translate import translate from odoo.tools.translate import _ @@ -63,7 +64,7 @@ def check(f): uid = 1 - if args and isinstance(args[0], (long, int)): + if args and isinstance(args[0], pycompat.integer_types): uid = args[0] lang = ctx and ctx.get('lang') @@ -83,7 +84,7 @@ def check(f): if args and len(args) > 1: # TODO self doesn't exist, but was already wrong before (it was not a registry but just the object_service. obj = self.get(args[1]) - if len(args) > 3 and isinstance(args[3], (long, int, list)): + if len(args) > 3 and isinstance(args[3], (pycompat.integer_types, list)): ids = args[3] else: ids = [] diff --git a/odoo/service/server.py b/odoo/service/server.py index 97d99983597..5de54d8188e 100644 --- a/odoo/service/server.py +++ b/odoo/service/server.py @@ -863,7 +863,7 @@ def _reexec(updated_modules=None): def load_test_file_yml(registry, test_file): with registry.cursor() as cr: - odoo.tools.convert_yaml_import(cr, 'base', file(test_file), 'test', {}, 'init') + odoo.tools.convert_yaml_import(cr, 'base', open(test_file, 'rb'), 'test', {}, 'init') if config['test_commit']: _logger.info('test %s has been commited', test_file) cr.commit() diff --git a/odoo/sql_db.py b/odoo/sql_db.py index a7c7d6fc54d..214dbfca96c 100644 --- a/odoo/sql_db.py +++ b/odoo/sql_db.py @@ -21,6 +21,8 @@ import psycopg2.extensions from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_REPEATABLE_READ from psycopg2.pool import PoolError +from .tools import pycompat + psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) _logger = logging.getLogger(__name__) @@ -255,10 +257,8 @@ class Cursor(object): sum = 0 if sqllogs[type]: sqllogitems = sqllogs[type].items() - sqllogitems.sort(key=lambda k: k[1][1]) _logger.debug("SQL LOG %s:", type) - sqllogitems.sort(lambda x, y: cmp(x[1][0], y[1][0])) - for r in sqllogitems: + for r in sorted(sqllogitems, key=lambda k: k[1]): delay = timedelta(microseconds=r[1][1]) _logger.debug("table: %s: %s/%s", r[0], delay, r[1][0]) sum += r[1][1] @@ -469,7 +469,7 @@ class LazyCursor(object): if cr is None: from odoo import registry cr = self._cursor = registry(self.dbname).cursor() - for _ in xrange(self._depth): + for _ in pycompat.range(self._depth): cr.__enter__() return getattr(cr, name) diff --git a/odoo/tests/common.py b/odoo/tests/common.py index c16ce8b0425..29bfdd79d0e 100644 --- a/odoo/tests/common.py +++ b/odoo/tests/common.py @@ -367,7 +367,7 @@ class HttpCase(TransactionCase): _logger.info('phantomjs unlink localstorage %s', i) os.unlink(i) try: - phantom = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=None) + phantom = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=None, close_fds=True) except OSError: raise unittest.SkipTest("PhantomJS not found") result = False diff --git a/odoo/tools/config.py b/odoo/tools/config.py index c832c1980cd..4443bc7e6d7 100644 --- a/odoo/tools/config.py +++ b/odoo/tools/config.py @@ -549,7 +549,7 @@ class configmanager(object): if not rc_exists and not os.path.exists(os.path.dirname(self.rcfile)): os.makedirs(os.path.dirname(self.rcfile)) try: - p.write(file(self.rcfile, 'w')) + p.write(open(self.rcfile, 'w')) if not rc_exists: os.chmod(self.rcfile, 0o600) except IOError: diff --git a/odoo/tools/convert.py b/odoo/tools/convert.py index 3410b929bc6..3df1d20b50f 100644 --- a/odoo/tools/convert.py +++ b/odoo/tools/convert.py @@ -890,10 +890,10 @@ def convert_xml_import(cr, module, xmlfile, idref=None, mode='init', noupdate=Fa if idref is None: idref={} - if isinstance(xmlfile, file): - xml_filename = xmlfile.name - else: + if isinstance(xmlfile, basestring): xml_filename = xmlfile + else: + xml_filename = xmlfile.name obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate, xml_filename=xml_filename) obj.parse(doc.getroot(), mode=mode) return True diff --git a/odoo/tools/float_utils.py b/odoo/tools/float_utils.py index 959299e2c97..8c71d7a29b2 100644 --- a/odoo/tools/float_utils.py +++ b/odoo/tools/float_utils.py @@ -4,6 +4,9 @@ from __future__ import print_function import math +from odoo.tools import pycompat + + def _float_check_precision(precision_digits=None, precision_rounding=None): assert (precision_digits is not None or precision_rounding is not None) and \ not (precision_digits and precision_rounding),\ @@ -54,7 +57,7 @@ def float_round(value, precision_digits=None, precision_rounding=None, rounding_ epsilon_magnitude = math.log(abs(normalized_value), 2) epsilon = 2**(epsilon_magnitude-53) if rounding_method == 'HALF-UP': - normalized_value += cmp(normalized_value,0) * epsilon + normalized_value += math.copysign(epsilon, normalized_value) rounded_value = round(normalized_value) # round to integer # TIE-BREAKING: UP (for ceiling operations) @@ -66,7 +69,7 @@ def float_round(value, precision_digits=None, precision_rounding=None, rounding_ # restored. elif rounding_method == 'UP': - sign = cmp(normalized_value, 0) + sign = math.copysign(1.0, normalized_value) normalized_value -= sign*epsilon rounded_value = math.ceil(abs(normalized_value))*sign # ceil to integer @@ -214,14 +217,13 @@ if __name__ == "__main__": fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555] expecteds = ['.00', '.02', '.01', '.68', '.67', '.46', '.456', '.4556'] precisions = [2, 2, 2, 2, 2, 2, 3, 4] - for magnitude in range(7): - for i in xrange(len(fractions)): - frac, exp, prec = fractions[i], expecteds[i], precisions[i] + for magnitude in pycompat.range(7): + for frac, exp, prec in zip(fractions, expecteds, precisions): for sign in [-1,1]: - for x in xrange(0,10000,97): + for x in pycompat.range(0, 10000, 97): n = x * 10**magnitude f = sign * (n + frac) - f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp + f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp try_round(f, f_exp, precision_digits=prec) stop = time.time() diff --git a/odoo/tools/graph.py b/odoo/tools/graph.py index 454a1dce98f..82fc0e0978a 100755 --- a/odoo/tools/graph.py +++ b/odoo/tools/graph.py @@ -634,7 +634,7 @@ class graph(object): self.init_order(self.start, self.result[self.start]['x']) for level in self.levels: - self.levels[level].sort(lambda x, y: cmp(self.result[x]['y'], self.result[y]['y'])) + self.levels[level].sort(key=lambda x: self.result[x]['y']) self.order_heuristic() self.process_order() diff --git a/odoo/tools/image.py b/odoo/tools/image.py index 952d6b25c4c..481abd3da02 100644 --- a/odoo/tools/image.py +++ b/odoo/tools/image.py @@ -280,6 +280,6 @@ if __name__=="__main__": assert len(sys.argv)==3, 'Usage to Test: image.py SRC.png DEST.png' - img = file(sys.argv[1],'rb').read().encode('base64') + img = open(sys.argv[1],'rb').read().encode('base64') new = image_resize_image(img, (128,100)) - file(sys.argv[2], 'wb').write(new.decode('base64')) + open(sys.argv[2], 'wb').write(new.decode('base64')) diff --git a/odoo/tools/import_email.py b/odoo/tools/import_email.py index 4178ca14a06..74029c693eb 100644 --- a/odoo/tools/import_email.py +++ b/odoo/tools/import_email.py @@ -125,7 +125,7 @@ class ReceiverEmail2Event(object): os.mkdir(newdir) counter += 1 - fd = file(os.path.join(newdir, filename), 'w') + fd = open(os.path.join(newdir, filename), 'w') fd.write(part.get_payload(decode=1)) fd.close() else: diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py index f080d3067ca..d90031e43eb 100644 --- a/odoo/tools/misc.py +++ b/odoo/tools/misc.py @@ -27,6 +27,7 @@ from cStringIO import StringIO from collections import defaultdict, Iterable, Mapping, MutableSet, OrderedDict from itertools import islice, izip, groupby, repeat from lxml import etree + from which import which from threading import local import traceback @@ -41,6 +42,7 @@ except ImportError: from config import config from cache import * from .parse_version import parse_version +from . import pycompat import odoo # get_encodings, ustr and exception_to_unicode were originally from tools.misc. @@ -279,7 +281,7 @@ def flatten(list): return r def reverse_enumerate(l): - """Like enumerate but in the other sens + """Like enumerate but in the other direction Usage:: >>> a = ['a', 'b', 'c'] @@ -295,7 +297,7 @@ def reverse_enumerate(l): File "", line 1, in StopIteration """ - return izip(xrange(len(l)-1, -1, -1), reversed(l)) + return izip(pycompat.range(len(l)-1, -1, -1), reversed(l)) def partition(pred, elems): """ Return a pair equivalent to: @@ -654,8 +656,11 @@ def posix_to_ldml(fmt, locale): def split_every(n, iterable, piece_maker=tuple): """Splits an iterable into length-n pieces. The last piece will be shorter if ``n`` does not evenly divide the iterable length. - @param ``piece_maker``: function to build the pieces - from the slices (tuple,list,...) + + :param int n: maximum size of each generated chunk + :param Iterable iterable: iterable to chunk into pieces + :param piece_maker: callable taking an iterable and collecting each + chunk from its slice, *must consume the entire slice*. """ iterator = iter(iterable) piece = piece_maker(islice(iterator, n)) @@ -990,6 +995,39 @@ class LastOrderedSet(OrderedSet): OrderedSet.discard(self, elem) OrderedSet.add(self, elem) +def unique(it): + """ "Uniquifier" for the provided iterable: will output each element of + the iterable once. + + The iterable's elements must be hashahble. + + :param Iterable it: + :rtype: Iterator + """ + seen = set() + for e in it: + if e not in seen: + seen.add(e) + yield e + +class Reverse(object): + """ Wraps a value and reverses its ordering, useful in key functions when + mixing ascending and descending sort on non-numeric data as the + ``reverse`` parameter can not do piecemeal reordering. + """ + __slots__ = ['val'] + + def __init__(self, val): + self.val = val + + def __eq__(self, other): return self.val == other.val + def __ne__(self, other): return self.val != other.val + + def __ge__(self, other): return self.val <= other.val + def __gt__(self, other): return self.val < other.val + def __le__(self, other): return self.val >= other.val + def __lt__(self, other): return self.val > other.val + @contextmanager def ignore(*exc): try: diff --git a/odoo/tools/pycompat.py b/odoo/tools/pycompat.py new file mode 100644 index 00000000000..114f7f2bd87 --- /dev/null +++ b/odoo/tools/pycompat.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# to remove if we decide to add a dependency on six or future +# very strongly inspired by https://github.com/pallets/werkzeug/blob/master/werkzeug/_compat.py +import sys + +PY2 = sys.version_info[0] + +if PY2: + # pylint: disable=long-builtin,xrange-builtin + integer_types = (int, long) + + range = xrange + + # noinspection PyUnresolvedReferences + from itertools import imap, izip, ifilter + + def implements_iterator(cls): + cls.next = cls.__next__ + del cls.__next__ + return cls +else: + integer_types = (int,) + + range = range + + imap = map + izip = zip + ifilter = filter + + def implements_iterator(cls): + return cls diff --git a/odoo/tools/safe_eval.py b/odoo/tools/safe_eval.py index 14020378c21..ebe97619e35 100644 --- a/odoo/tools/safe_eval.py +++ b/odoo/tools/safe_eval.py @@ -16,11 +16,14 @@ condition/math builtins. # - safe_eval in tryton http://hg.tryton.org/hgwebdir.cgi/trytond/rev/bbb5f73319ad from opcode import HAVE_ARGUMENT, opmap + +import functools from psycopg2 import OperationalError from types import CodeType import logging import werkzeug +from odoo.tools import pycompat from .misc import ustr import odoo @@ -226,7 +229,6 @@ _BUILTINS = { 'bool': bool, 'int': int, 'float': float, - 'long': long, 'enumerate': enumerate, 'dict': dict, 'list': list, @@ -236,7 +238,7 @@ _BUILTINS = { 'min': min, 'max': max, 'sum': sum, - 'reduce': reduce, + 'reduce': functools.reduce, 'filter': filter, 'round': round, 'len': len, @@ -246,11 +248,10 @@ _BUILTINS = { 'any': any, 'ord': ord, 'chr': chr, - 'cmp': cmp, 'divmod': divmod, 'isinstance': isinstance, 'range': range, - 'xrange': xrange, + 'xrange': pycompat.range, 'zip': zip, 'Exception': Exception, } diff --git a/odoo/tools/test_reports.py b/odoo/tools/test_reports.py index ef09862bc01..c1f9af8bb55 100644 --- a/odoo/tools/test_reports.py +++ b/odoo/tools/test_reports.py @@ -11,6 +11,8 @@ import odoo import odoo.report import odoo.tools as tools import logging + +from odoo.tools import pycompat from odoo.tools.safe_eval import safe_eval from subprocess import Popen, PIPE import os @@ -45,7 +47,7 @@ def try_report(cr, uid, rname, ids, data=None, context=None, our_module=None, re raise ValueError("Report %s produced an empty result!" % rname) if tools.config['test_report_directory']: - file(os.path.join(tools.config['test_report_directory'], rname+ '.'+res_format), 'wb+').write(res_data) + open(os.path.join(tools.config['test_report_directory'], rname+ '.'+res_format), 'wb+').write(res_data) _logger.debug("Have a %s report for %s, will examine it", res_format, rname) if res_format == 'pdf': @@ -128,7 +130,7 @@ def try_report_action(cr, uid, action_id, active_model=None, active_ids=None, action = env.ref(action_id) act_model, act_id = action._name, action.id else: - assert isinstance(action_id, (long, int)) + assert isinstance(action_id, pycompat.integer_types) act_model = 'ir.action.act_window' # assume that act_id = action_id act_xmlid = '<%s>' % act_id diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py index 16a7bd9015e..75495b4bbae 100644 --- a/odoo/tools/translate.py +++ b/odoo/tools/translate.py @@ -20,7 +20,7 @@ from babel.messages import extract from lxml import etree import odoo -from odoo.tools import config +from odoo.tools import config, pycompat from odoo.tools.misc import file_open, get_iso_codes, SKIPPED_ELEMENT_TYPES from odoo.tools.osutil import walksymlinks from odoo import sql_db, SUPERUSER_ID @@ -679,7 +679,7 @@ def trans_export(lang, modules, buffer, format, cr): tmpmoddir = join(tmpdir, mod, 'i18n') os.makedirs(tmpmoddir) pofilename = (lang if lang else mod) + ".po" + ('t' if not lang else '') - buf = file(join(tmpmoddir, pofilename), 'w') + buf = open(join(tmpmoddir, pofilename), 'w') _process('po', [mod], modrows, buf, lang) buf.close() @@ -1106,7 +1106,7 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, if not res_id: return - if isinstance(res_id, (int, long)) or \ + if isinstance(res_id, pycompat.integer_types) or \ (isinstance(res_id, basestring) and res_id.isdigit()): dic['res_id'] = int(res_id) if module_name: diff --git a/setup.py b/setup.py index 1b63d36bd5a..38001480fbf 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ from setuptools import find_packages, setup from os.path import join, dirname -execfile(join(dirname(__file__), 'odoo', 'release.py')) # Load release variables +exec(open(join(dirname(__file__), 'odoo', 'release.py'), 'rb').read()) # Load release variables lib_name = 'odoo' diff --git a/setup/package.py b/setup/package.py index 6dedb2b78d8..fcb174435ac 100755 --- a/setup/package.py +++ b/setup/package.py @@ -22,7 +22,7 @@ from tempfile import NamedTemporaryFile #---------------------------------------------------------- # Utils #---------------------------------------------------------- -execfile(join(dirname(__file__), '..', 'odoo', 'release.py')) +exec(open(join(dirname(__file__), '..', 'odoo', 'release.py'), 'rb').read()) version = version.split('-')[0] docker_version = version.replace('+', '') timestamp = time.strftime("%Y%m%d", time.gmtime()) diff --git a/setup/win32/win32_service.py b/setup/win32/win32_service.py index 76b21f4050f..8eb32bbdbb6 100644 --- a/setup/win32/win32_service.py +++ b/setup/win32/win32_service.py @@ -11,7 +11,7 @@ import sys from os.path import dirname, join, split -execfile(join(dirname(__file__), '..', 'server', 'odoo', 'release.py')) +exec(open(join(dirname(__file__), '..', 'server', 'odoo', 'release.py'), 'rb').read()) class OdooService(win32serviceutil.ServiceFramework): diff --git a/setup/win32/win32_setup.py b/setup/win32/win32_setup.py index d973f90674a..a61081760d0 100644 --- a/setup/win32/win32_setup.py +++ b/setup/win32/win32_setup.py @@ -6,7 +6,7 @@ import py2exe from distutils.core import setup -execfile(os.path.join(os.path.dirname(__file__), '..', '..', 'odoo', 'release.py')) +exec(open(os.path.join(os.path.dirname(__file__), '..', '..', 'odoo', 'release.py'), 'rb').read()) def generate_files():