diff --git a/addons/account/models/account_analytic_line.py b/addons/account/models/account_analytic_line.py index 20b44c04dbb..1d212d31bf8 100644 --- a/addons/account/models/account_analytic_line.py +++ b/addons/account/models/account_analytic_line.py @@ -44,7 +44,7 @@ class AccountAnalyticLine(models.Model): # Compute based on pricetype amount_unit = self.product_id.price_compute('standard_price', uom=unit)[self.product_id.id] amount = amount_unit * self.unit_amount or 0.0 - result = round(amount, self.currency_id.decimal_places) * -1 + result = self.currency_id.round(amount) * -1 self.amount = result self.general_account_id = account self.product_uom_id = unit diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 24e8abc3049..37ec02186e9 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -1551,14 +1551,13 @@ class AccountPaymentTerm(models.Model): currency = self.env['res.currency'].browse(self.env.context['currency_id']) else: currency = self.env.user.company_id.currency_id - prec = currency.decimal_places for line in self.line_ids: if line.value == 'fixed': - amt = round(line.value_amount, prec) + amt = currency.round(line.value_amount) elif line.value == 'percent': - amt = round(value * (line.value_amount / 100.0), prec) + amt = currency.round(value * (line.value_amount / 100.0)) elif line.value == 'balance': - amt = round(amount, prec) + amt = currency.round(amount) if amt: next_date = fields.Date.from_string(date_ref) if line.option == 'day_after_invoice_date': @@ -1573,7 +1572,7 @@ class AccountPaymentTerm(models.Model): result.append((fields.Date.to_string(next_date), amt)) amount -= amt amount = sum(amt for _, amt in result) - dist = round(value - amount, prec) + dist = currency.round(value - amount) if dist: last_date = result and result[-1][0] or fields.Date.today() result.append((last_date, dist)) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index a47f232f3f2..ca46bbb5612 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -192,10 +192,10 @@ class AccountMove(models.Model): @api.multi def _check_lock_date(self): for move in self: - lock_date = max(move.company_id.period_lock_date, move.company_id.fiscalyear_lock_date) + lock_date = max(move.company_id.period_lock_date or '0000-00-00', move.company_id.fiscalyear_lock_date or '0000-00-00') if self.user_has_groups('account.group_account_manager'): lock_date = move.company_id.fiscalyear_lock_date - if move.date <= lock_date: + if move.date <= (lock_date or '0000-00-00'): if self.user_has_groups('account.group_account_manager'): message = _("You cannot add/modify entries prior to and inclusive of the lock date %s") % (lock_date) else: @@ -1029,7 +1029,7 @@ class AccountMoveLine(models.Model): partial_rec_set = self.env['account.partial.reconcile'] aml_id = False partial_rec_id = False - maxdate = None + maxdate = '0000-00-00' for aml in self: total_debit += aml.debit total_credit += aml.credit @@ -1491,7 +1491,7 @@ class AccountPartialReconcile(models.Model): # The move date should be the maximum date between payment and invoice # (in case of payment in advance). However, we should make sure the # move date is not recorded after the end of year closing. - if move_date > self.company_id.fiscalyear_lock_date: + if (move_date or '0000-00-00' > self.company_id.fiscalyear_lock_date or '0000-00-00'): res['date'] = move_date return res @@ -1689,7 +1689,7 @@ class AccountPartialReconcile(models.Model): #make sure that all partial reconciliations share the same secondary currency otherwise it's not #possible to compute the exchange difference entry and it has to be done manually. currency = list(partial_rec_set)[0].currency_id - maxdate = None + maxdate = '0000-00-00' aml_to_balance = None for partial_rec in partial_rec_set: if partial_rec.currency_id != currency: diff --git a/addons/account/report/account_partner_ledger.py b/addons/account/report/account_partner_ledger.py index b3e0b03b6ba..f22ef71527c 100644 --- a/addons/account/report/account_partner_ledger.py +++ b/addons/account/report/account_partner_ledger.py @@ -109,7 +109,7 @@ class ReportPartnerLedger(models.AbstractModel): self.env.cr.execute(query, tuple(params)) partner_ids = [res['partner_id'] for res in self.env.cr.dictfetchall()] partners = obj_partner.browse(partner_ids) - partners = sorted(partners, key=lambda x: (x.ref, x.name)) + partners = sorted(partners, key=lambda x: (x.ref or '', x.name or '')) return { 'doc_ids': partner_ids, diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 040ddd097d3..76f8a286205 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -298,9 +298,9 @@ class Lead(models.Model): # stage change with new stage: update probability and date_closed if vals.get('stage_id') and 'probability' not in vals: vals.update(self._onchange_stage_id_values(vals.get('stage_id'))) - if vals.get('probability') >= 100 or not vals.get('active', True): + if vals.get('probability', 0) >= 100 or not vals.get('active', True): vals['date_closed'] = fields.Datetime.now() - elif 'probability' in vals and vals['probability'] < 100: + elif 'probability' in vals: vals['date_closed'] = False return super(Lead, self).write(vals) diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index 95165441dfd..cfa5654b0cd 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -7,7 +7,6 @@ import logging import psycopg2 import werkzeug -from operator import itemgetter from werkzeug import url_encode from odoo import api, http, registry, SUPERUSER_ID, _ @@ -154,10 +153,10 @@ class MailController(http.Controller): 'default': subtype.default, 'internal': subtype.internal, 'followed': subtype.id in followers.mapped('subtype_ids').ids, - 'parent_model': subtype.parent_id and subtype.parent_id.res_model or False, + 'parent_model': subtype.parent_id.res_model, 'id': subtype.id } for subtype in subtypes] - subtypes_list = sorted(subtypes_list, key=itemgetter('parent_model', 'res_model', 'internal', 'sequence')) + subtypes_list = sorted(subtypes_list, key=lambda it: (it['parent_model'] or '', it['res_model'] or '', it['internal'], it['sequence'])) return subtypes_list @http.route('/mail/view', type='http', auth='none') diff --git a/addons/membership/models/account_invoice.py b/addons/membership/models/account_invoice.py index 273dd4290ba..73622ce0fb2 100644 --- a/addons/membership/models/account_invoice.py +++ b/addons/membership/models/account_invoice.py @@ -60,9 +60,9 @@ class AccountInvoiceLine(models.Model): invoice_line.product_id.membership and \ not MemberLine.search([('account_invoice_line', '=', invoice_line.id)]): # Product line is a membership product - date_from = invoice_line.product_id.membership_date_from - date_to = invoice_line.product_id.membership_date_to - if invoice_line.invoice_id.date_invoice > date_from and invoice_line.invoice_id.date_invoice < date_to: + date_from = invoice_line.product_id.membership_date_from or '0000-00-00' + date_to = invoice_line.product_id.membership_date_to or '0000-00-00' + if date_from < (invoice_line.invoice_id.date_invoice or '0000-00-00') < date_to: date_from = invoice_line.invoice_id.date_invoice MemberLine.create({ 'partner': invoice_line.invoice_id.partner_id.id, diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py index c1c62f32da6..350e8ad8684 100644 --- a/addons/purchase/models/account_invoice.py +++ b/addons/purchase/models/account_invoice.py @@ -146,7 +146,6 @@ class AccountInvoice(models.Model): # reference_account_id is the stock input account reference_account_id = i_line.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=fpos)['stock_input'].id diff_res = [] - account_prec = inv.company_id.currency_id.decimal_places # calculate and write down the possible price difference between invoice price and product price for line in res: if line.get('invl_id', 0) == i_line.id and reference_account_id == line['account_id']: @@ -180,13 +179,13 @@ class AccountInvoice(models.Model): if child.type_tax_use != 'none': tax_ids.append((4, child.id, None)) price_before = line.get('price', 0.0) - line.update({'price': round(valuation_price_unit * line['quantity'], account_prec)}) + line.update({'price': company_currency.round(valuation_price_unit * line['quantity'])}) diff_res.append({ 'type': 'src', 'name': i_line.name[:64], - 'price_unit': round(price_unit - valuation_price_unit, account_prec), + 'price_unit': company_currency.round(price_unit - valuation_price_unit), 'quantity': line['quantity'], - 'price': round(price_before - line.get('price', 0.0), account_prec), + 'price': company_currency.round(price_before - line.get('price', 0.0)), 'account_id': acc, 'product_id': line['product_id'], 'uom_id': line['uom_id'], diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py index cd26abaabc8..b50faa855fc 100644 --- a/addons/resource/models/resource.py +++ b/addons/resource/models/resource.py @@ -420,8 +420,12 @@ class ResourceCalendar(models.Model): dtstart=start_dt, until=end_dt, byweekday=self._get_weekdays()): - start_time = day.date() == start_dt.date() and start_dt.time() or datetime.time.min - end_time = day.date() == end_dt.date() and end_dt.time() or datetime.time.max + start_time = datetime.time.min + if day.date() == start_dt.date(): + start_time = start_dt.time() + end_time = datetime.time.max + if day.date() == end_dt.date() and end_dt.time() != datetime.time(): + end_time = end_dt.time() intervals = self._get_day_work_intervals( day.date(), @@ -445,8 +449,12 @@ class ResourceCalendar(models.Model): dtstart=start_dt, until=end_dt, byweekday=self._get_weekdays()): - start_time = day.date() == start_dt.date() and start_dt.time() or datetime.time.min - end_time = day.date() == end_dt.date() and end_dt.time() or datetime.time.max + start_time = datetime.time.min + if day.date() == start_dt.date(): + start_time = start_dt.time() + end_time = datetime.time.max + if day.date() == end_dt.date() and end_dt.time() != datetime.time(): + end_time = end_dt.time() intervals = self._get_day_leave_intervals( day.date(), diff --git a/addons/resource/models/resource_mixin.py b/addons/resource/models/resource_mixin.py index 8b922dd5ff8..0d45ec646a1 100644 --- a/addons/resource/models/resource_mixin.py +++ b/addons/resource/models/resource_mixin.py @@ -4,7 +4,7 @@ from datetime import timedelta from odoo import api, fields, models - +from odoo.tools import pycompat class ResourceMixin(models.AbstractModel): _name = "resource.mixin" @@ -47,7 +47,7 @@ class ResourceMixin(models.AbstractModel): theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar) work_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta()) total_work_time += work_time - days_count += round((work_time.total_seconds() / 3600 / theoric_hours) * 4) / 4 + days_count += pycompat.round((work_time.total_seconds() / 3600 / theoric_hours) * 4) / 4 return { 'days': days_count, 'hours': total_work_time.total_seconds() / 3600, @@ -70,7 +70,7 @@ class ResourceMixin(models.AbstractModel): for day_intervals in calendar._iter_leave_intervals(from_datetime, to_datetime, self.resource_id.id): theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar) leave_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta()) - days_count += round((leave_time.total_seconds()/3600 / theoric_hours) * 4) / 4 + days_count += pycompat.round((leave_time.total_seconds() / 3600 / theoric_hours) * 4) / 4 return days_count def iter_leaves(self, from_datetime, to_datetime, calendar=None): diff --git a/addons/stock_account/models/account_invoice.py b/addons/stock_account/models/account_invoice.py index 6291da2922f..281d15de0e3 100644 --- a/addons/stock_account/models/account_invoice.py +++ b/addons/stock_account/models/account_invoice.py @@ -94,7 +94,7 @@ class AccountInvoiceLine(models.Model): price = company_currency.with_context(date=self.invoice_id.date_invoice).compute(price_unit * self.quantity, self.invoice_id.currency_id) else: price = price_unit * self.quantity - return round(price, self.invoice_id.currency_id.decimal_places) + return self.invoice_id.currency_id.round(price) def get_invoice_line_account(self, type, product, fpos, company): if company.anglo_saxon_accounting and type in ('in_invoice', 'in_refund') and product and product.type == 'product': diff --git a/addons/survey/tests/test_survey.py b/addons/survey/tests/test_survey.py index 699aac0cde3..04a65a02562 100644 --- a/addons/survey/tests/test_survey.py +++ b/addons/survey/tests/test_survey.py @@ -226,9 +226,7 @@ class TestSurvey(TransactionCase): lines = [line.value_suggested.id for line in question.user_input_line_ids] answers = [{'text': label.value, 'count': lines.count(label.id), 'answer_id': label.id} for label in question.labels_ids] prp_result = self.env['survey.survey'].prepare_result(question)['answers'] - answers.sort() - prp_result.sort() - self.assertEqual(prp_result, answers, msg="Statistics of simple, multiple choice questions are different from expectation") + self.assertItemsEqual(prp_result, answers, msg="Statistics of simple, multiple choice questions are different from expectation") def test_11_survey_result_matrix(self): question = self.env['survey.question'].sudo(self.survey_manager).create({ diff --git a/addons/web_editor/models/ir_qweb.py b/addons/web_editor/models/ir_qweb.py index fcab2902a3b..4cceec10421 100644 --- a/addons/web_editor/models/ir_qweb.py +++ b/addons/web_editor/models/ir_qweb.py @@ -510,7 +510,7 @@ def _realize_padding(it): requests for at least n newlines of padding. Runs thereof can be collapsed into the largest requests and converted to newlines. """ - padding = None + padding = 0 for item in it: if isinstance(item, int): padding = max(padding, item) @@ -518,7 +518,7 @@ def _realize_padding(it): if padding: yield '\n' * padding - padding = None + padding = 0 yield item # leftover padding irrelevant as the output will be stripped diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py index 22456302949..979ae14c5d3 100644 --- a/addons/website_blog/models/website_blog.py +++ b/addons/website_blog/models/website_blog.py @@ -209,7 +209,7 @@ class BlogPost(models.Model): def write(self, vals): self.ensure_one() if 'website_published' in vals and 'published_date' not in vals: - if self.published_date <= fields.Datetime.now(): + if (self.published_date or '') <= fields.Datetime.now(): vals['published_date'] = vals['website_published'] and fields.Datetime.now() result = super(BlogPost, self).write(vals) self._check_for_publication(vals) diff --git a/addons/website_customer/controllers/main.py b/addons/website_customer/controllers/main.py index d6b10b37dc1..e7318a64abf 100644 --- a/addons/website_customer/controllers/main.py +++ b/addons/website_customer/controllers/main.py @@ -73,7 +73,7 @@ class WebsiteCustomer(http.Controller): 'country_id_count': 0, 'country_id': (country.id, country.name) }) - countries.sort(key=lambda d: d['country_id'] and d['country_id'][1]) + countries.sort(key=lambda d: (d['country_id'] or (0, ""))[1]) countries.insert(0, { 'country_id_count': country_count, diff --git a/addons/website_event_track/controllers/main.py b/addons/website_event_track/controllers/main.py index 3fb37210589..1864b28379c 100644 --- a/addons/website_event_track/controllers/main.py +++ b/addons/website_event_track/controllers/main.py @@ -68,7 +68,7 @@ class WebsiteEventTrackController(http.Controller): @http.route(['''/event/Our Sponsors
-
+
', 1), ('website_published', '=', True)], limit=step, offset=pager['offset'], order='karma DESC') # put the users in block of 3 to display them as a table - users = [[] for i in range(len(user_obj) / 3 + 1)] + users = [[] for i in range(len(user_obj) // 3 + 1)] for index, user in enumerate(user_obj): - users[index / 3].append(user) + users[index // 3].append(user) searches['users'] = 'True' values = self._prepare_forum_values(forum=forum, searches=searches) diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 05abe7627b1..8ec73048832 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -53,28 +53,28 @@ class TableCompute(object): x = y = 1 pos = minpos - while not self._check_place(pos % PPR, pos / PPR, x, y): + while not self._check_place(pos % PPR, pos // PPR, x, y): pos += 1 # if 21st products (index 20) and the last line is full (PPR products in it), break # (pos + 1.0) / PPR is the line where the product would be inserted # maxy is the number of existing lines # + 1.0 is because pos begins at 0, thus pos 20 is actually the 21st block # and to force python to not round the division operation - if index >= ppg and ((pos + 1.0) / PPR) > maxy: + if index >= ppg and ((pos + 1.0) // PPR) > maxy: break if x == 1 and y == 1: # simple heuristic for CPU optimization - minpos = pos / PPR + minpos = pos // PPR for y2 in range(y): for x2 in range(x): - self.table[(pos / PPR) + y2][(pos % PPR) + x2] = False - self.table[pos / PPR][pos % PPR] = { + self.table[(pos // PPR) + y2][(pos % PPR) + x2] = False + self.table[pos // PPR][pos % PPR] = { 'product': p, 'x': x, 'y': y, 'class': " ".join(x.html_class for x in p.website_style_ids if x.html_class) } if index <= ppg: - maxy = max(maxy, y + (pos / PPR)) + maxy = max(maxy, y + (pos // PPR)) index += 1 # Format table according to HTML needs @@ -909,7 +909,7 @@ class WebsiteSale(http.Controller): @http.route(['/shop/get_unit_price'], type='json', auth="public", methods=['POST'], website=True) def get_unit_price(self, product_ids, add_qty, **kw): products = request.env['product.product'].with_context({'quantity': add_qty}).browse(product_ids) - return {product.id: product.website_price / add_qty for product in products} + return {product.id: product.website_price // add_qty for product in products} # ------------------------------------------------------ # Edit diff --git a/odoo/addons/base/ir/ir_http.py b/odoo/addons/base/ir/ir_http.py index ff4654cf6c9..0aa1abc52ca 100644 --- a/odoo/addons/base/ir/ir_http.py +++ b/odoo/addons/base/ir/ir_http.py @@ -323,7 +323,7 @@ class IrHttp(models.AbstractModel): headers += [('Content-Type', mimetype), ('X-Content-Type-Options', 'nosniff')] # cache - etag = hasattr(request, 'httprequest') and request.httprequest.headers.get('If-None-Match') + etag = bool(request) and request.httprequest.headers.get('If-None-Match') retag = '"%s"' % hashlib.md5(last_update).hexdigest() status = status or (304 if etag == retag else 200) headers.append(('ETag', retag)) diff --git a/odoo/addons/base/tests/test_api.py b/odoo/addons/base/tests/test_api.py index 55cf1ff26b5..ad2dd1f0e85 100644 --- a/odoo/addons/base/tests/test_api.py +++ b/odoo/addons/base/tests/test_api.py @@ -482,7 +482,7 @@ class TestAPI(common.TransactionCase): ps = self.env['res.partner'].search([]) # sort by model order - qs = ps[:len(ps) / 2] + ps[len(ps) / 2:] + qs = ps[:len(ps) // 2] + ps[len(ps) // 2:] self.assertEqual(qs.sorted().ids, ps.ids) # sort by name, with a function or a field name diff --git a/odoo/addons/test_read_group/tests/test_empty.py b/odoo/addons/test_read_group/tests/test_empty.py index c9ee1c2a40a..11142b257e8 100644 --- a/odoo/addons/test_read_group/tests/test_empty.py +++ b/odoo/addons/test_read_group/tests/test_empty.py @@ -46,7 +46,7 @@ class TestEmptyDate(common.TransactionCase): gb = self.Model.read_group([], ['date', 'value'], ['date'], lazy=False) - self.assertSequenceEqual(sorted(gb, key=lambda r: r['date']), [{ + self.assertSequenceEqual(sorted(gb, key=lambda r: r['date'] or ''), [{ '__count': 2, '__domain': [('date', '=', False)], 'date': False, diff --git a/odoo/modules/module.py b/odoo/modules/module.py index e6194a5fb10..f0836cbe041 100644 --- a/odoo/modules/module.py +++ b/odoo/modules/module.py @@ -149,8 +149,8 @@ def initialize_sys_path(): ad_paths.append(ad) if not hooked: - sys.meta_path.append(AddonsHook()) - sys.meta_path.append(OdooHook()) + sys.meta_path.insert(0, OdooHook()) + sys.meta_path.insert(0, AddonsHook()) hooked = True def get_module_path(module, downloaded=False, display_warning=True): diff --git a/odoo/tools/float_utils.py b/odoo/tools/float_utils.py index 9c37da07fe9..d71576adc92 100644 --- a/odoo/tools/float_utils.py +++ b/odoo/tools/float_utils.py @@ -58,7 +58,7 @@ def float_round(value, precision_digits=None, precision_rounding=None, rounding_ epsilon = 2**(epsilon_magnitude-53) if rounding_method == 'HALF-UP': normalized_value += math.copysign(epsilon, normalized_value) - rounded_value = round(normalized_value) # round to integer + rounded_value = pycompat.round(normalized_value) # round to integer # TIE-BREAKING: UP (for ceiling operations) # When rounding the value up, we instead subtract the epsilon value diff --git a/odoo/tools/image.py b/odoo/tools/image.py index d6dd1488aba..791d1a978d3 100644 --- a/odoo/tools/image.py +++ b/odoo/tools/image.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - try: import cStringIO as StringIO except ImportError: @@ -102,7 +101,7 @@ def image_resize_and_sharpen(image, size, preserve_aspect_ratio=False, factor=2. resized_image = sharpener.enhance(factor) # create a transparent image for background and paste the image on it image = Image.new('RGBA', size, (255, 255, 255, 0)) - image.paste(resized_image, ((size[0] - resized_image.size[0]) / 2, (size[1] - resized_image.size[1]) / 2)) + image.paste(resized_image, ((size[0] - resized_image.size[0]) // 2, (size[1] - resized_image.size[1]) // 2)) if image.mode != origin_mode: image = image.convert(origin_mode) return image @@ -181,17 +180,17 @@ def crop_image(data, type='top', ratio=False, thumbnail_ratio=None, image_format if ratio: w_ratio, h_ratio = ratio - new_h = (w * h_ratio) / w_ratio + new_h = (w * h_ratio) // w_ratio new_w = w if new_h > h: new_h = h - new_w = (h * w_ratio) / h_ratio + new_w = (h * w_ratio) // h_ratio if type == "top": cropped_image = image_stream.crop((0, 0, new_w, new_h)) cropped_image.save(output_stream, format=image_format) elif type == "center": - cropped_image = image_stream.crop(((w - new_w) / 2, (h - new_h) / 2, (w + new_w) / 2, (h + new_h) / 2)) + cropped_image = image_stream.crop(((w - new_w) // 2, (h - new_h) // 2, (w + new_w) // 2, (h + new_h) // 2)) cropped_image.save(output_stream, format=image_format) elif type == "bottom": cropped_image = image_stream.crop((0, h - new_h, new_w, h)) @@ -201,7 +200,7 @@ def crop_image(data, type='top', ratio=False, thumbnail_ratio=None, image_format # TDE FIXME: should not have a ratio, makes no sense -> should have maximum width (std: 64; 256 px) if thumbnail_ratio: thumb_image = Image.open(StringIO.StringIO(output_stream.getvalue())) - thumb_image.thumbnail((new_w / thumbnail_ratio, new_h / thumbnail_ratio), Image.ANTIALIAS) + thumb_image.thumbnail((new_w // thumbnail_ratio, new_h // thumbnail_ratio), Image.ANTIALIAS) thumb_image.save(output_stream, image_format) return output_stream.getvalue().encode('base64') diff --git a/odoo/tools/pycompat.py b/odoo/tools/pycompat.py index 7ce4f45773a..19b821863b2 100644 --- a/odoo/tools/pycompat.py +++ b/odoo/tools/pycompat.py @@ -8,6 +8,7 @@ PY2 = sys.version_info[0] == 2 if PY2: # pylint: disable=long-builtin,dict-iter-method integer_types = (int, long) + round = round keys = lambda d: iter(d.iterkeys()) values = lambda d: iter(d.itervalues()) @@ -23,8 +24,24 @@ if PY2: exec ('def reraise(tp, value, tb=None):\n raise tp, value, tb') else: + import builtins, math # pylint: disable=bad-functions integer_types = (int,) + def round(f): + # P3's builtin round differs from P2 in the following manner: + # * it rounds half to even rather than up (away from 0) + # * round(-0.) loses the sign (it returns -0 rather than 0) + # * round(x) returns an int rather than a float + # + # this compatibility shim implements Python 2's round in terms of + # Python 3's so that important rounding error under P3 can be + # trivially fixed, assuming the P2 behaviour to be debugged and + # correct. + roundf = builtins.round(f) + if builtins.round(f + 1) - roundf != 1: + return f + math.copysign(0.5, f) + # copysign ensures round(-0.) -> -0 *and* result is a float + return math.copysign(roundf, f) keys = lambda d: iter(d.keys()) values = lambda d: iter(d.values())