diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index 3a2e0d1d7aa..1b3f03966f3 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -95,7 +95,7 @@ class Web_Editor(http.Controller): font_obj = ImageFont.truetype(addons_path + font, size) # if received character is not a number, keep old behaviour (icon is character) - icon = pycompat.unichr(int(icon)) if icon.isdigit() else icon + icon = chr(int(icon)) if icon.isdigit() else icon # Determine the dimensions of the icon image = Image.new("RGBA", (size, size), color=(0, 0, 0, 0)) diff --git a/doc/_extensions/pyjsparser/parser.py b/doc/_extensions/pyjsparser/parser.py index 28001886a8e..d47a94a8522 100644 --- a/doc/_extensions/pyjsparser/parser.py +++ b/doc/_extensions/pyjsparser/parser.py @@ -27,16 +27,8 @@ __all__ = ['PyJsParser', 'parse', 'ENABLE_JS2PY_ERRORS', 'ENABLE_PYIMPORT', 'JsS REGEXP_SPECIAL_SINGLE = ('\\', '^', '$', '*', '+', '?', '.', '[', ']', '(', ')', '{', '{', '|', '-') ENABLE_PYIMPORT = False ENABLE_JS2PY_ERRORS = False - -PY3 = sys.version_info >= (3,0) - -if PY3: - basestring = str - long = int - xrange = range - unicode = str - ESPRIMA_VERSION = '2.2.0' + DEBUG = False # Small naming convention changes # len -> leng @@ -179,7 +171,7 @@ class PyJsParser: def scanHexEscape(self, prefix): code = 0 leng = 4 if (prefix == 'u') else 2 - for i in xrange(leng): + for i in range(leng): if self.index < self.length and isHexDigit(self.source[self.index]): ch = self.source[self.index] self.index += 1 @@ -442,7 +434,7 @@ class PyJsParser: def isImplicitOctalLiteral(self): # Implicit octal, unless there is a non-octal digit. # (Annex B.1.1 on Numeric Literals) - for i in xrange(self.index + 1, self.length): + for i in range(self.index + 1, self.length): ch = self.source[i]; if (ch == '8' or ch == '9'): return False; @@ -992,20 +984,20 @@ class PyJsParser: except: raise Exception("ENABLE_JS2PY_ERRORS was set to True, but Js2Py was not found!") ENABLE_PYIMPORT = old_pyimport - error = ERRORS['SyntaxError']('Line ' + unicode(line) + ': ' + unicode(description)) + error = ERRORS['SyntaxError']('Line ' + str(line) + ': ' + str(description)) error.put('index', Js(pos)) error.put('lineNumber', Js(line)) error.put('column', Js(pos - (self.lineStart if self.scanning else self.lastLineStart) + 1)) error.put('description', Js(description)) return JsToPyException(error) else: - return JsSyntaxError('Line ' + unicode(line) + ': ' + unicode(description)) + return JsSyntaxError('Line ' + str(line) + ': ' + str(description)) # Throw an exception def throwError(self, messageFormat, *args): - msg = messageFormat % tuple(unicode(e) for e in args) + msg = messageFormat % tuple(str(e) for e in args) raise self.createError(self.lastLineNumber, self.lastIndex, msg); def tolerateError(self, messageFormat, *args): @@ -1038,7 +1030,7 @@ class PyJsParser: value = token['value']['raw'] if (typ == Token.Template) else token.get('value') else: value = 'ILLEGAL' - msg = msg.replace('%s', unicode(value)) + msg = msg.replace('%s', str(value)) return (self.createError(token['lineNumber'], token['start'], msg) if (token and token.get('lineNumber')) else self.createError(self.lineNumber if self.scanning else self.lastLineNumber, @@ -1445,12 +1437,12 @@ class PyJsParser: self.reinterpretExpressionAsPattern(expr.argument) elif typ == Syntax.ArrayExpression: expr['type'] = Syntax.ArrayPattern - for i in xrange(len(expr['elements'])): + for i in range(len(expr['elements'])): if (expr['elements'][i] != null): self.reinterpretExpressionAsPattern(expr['elements'][i]) elif typ == Syntax.ObjectExpression: expr['type'] = Syntax.ObjectPattern - for i in xrange(len(expr['properties'])): + for i in range(len(expr['properties'])): self.reinterpretExpressionAsPattern(expr['properties'][i]['value']); elif Syntax.AssignmentExpression: expr['type'] = Syntax.AssignmentPattern; @@ -1526,7 +1518,7 @@ class PyJsParser: if (not self.match('=>')): self.expect('=>'); self.isBindingElement = false - for i in xrange(len(expressions)): + for i in range(len(expressions)): self.reinterpretExpressionAsPattern(expressions[i]) return { 'type': PlaceHolders.ArrowParameterPlaceHolder, @@ -1539,7 +1531,7 @@ class PyJsParser: if (not self.isBindingElement): self.throwUnexpectedToken(self.lookahead); if (expr['type'] == Syntax.SequenceExpression): - for i in xrange(len(expr.expressions)): + for i in range(len(expr.expressions)): self.reinterpretExpressionAsPattern(expr['expressions'][i]) else: self.reinterpretExpressionAsPattern(expr); @@ -1880,12 +1872,12 @@ class PyJsParser: elif typ == Syntax.AssignmentPattern: self.checkPatternParam(options, param.left) elif typ == Syntax.ArrayPattern: - for i in xrange(len(param.elements)): + for i in range(len(param.elements)): if (param.elements[i] != null): self.checkPatternParam(options, param.elements[i]); else: assert typ == Syntax.ObjectPattern, 'Invalid type' - for i in xrange(len(param.properties)): + for i in range(len(param.properties)): self.checkPatternParam(options, param.properties[i]['value']); def reinterpretAsCoverFormalsList(self, expr): @@ -1902,7 +1894,7 @@ class PyJsParser: options = { 'paramSet': {}} le = len(params) - for i in xrange(le): + for i in range(le): param = params[i] if param.type == Syntax.AssignmentPattern: params[i] = param.left; @@ -2870,7 +2862,7 @@ class PyJsParser: raise NotImplementedError('Options not implemented! You can only use default settings.') self.clean() - self.source = unicode(code) + ' \n ; //END' # I have to add it in order not to check for EOF every time + self.source = str(code) + ' \n ; //END' # I have to add it in order not to check for EOF every time self.index = 0 self.lineNumber = 1 if len(self.source) > 0 else 0 self.lineStart = 0 diff --git a/doc/_extensions/pyjsparser/pyjsparserdata.py b/doc/_extensions/pyjsparser/pyjsparserdata.py index 6ca6dbc47cc..5db295a34b5 100644 --- a/doc/_extensions/pyjsparser/pyjsparserdata.py +++ b/doc/_extensions/pyjsparser/pyjsparserdata.py @@ -23,12 +23,6 @@ import sys import unicodedata from collections import defaultdict -PY3 = sys.version_info >= (3,0) - -if PY3: - unichr = chr - xrange = range - unicode = str token = { 'BooleanLiteral': 1, @@ -221,7 +215,7 @@ LS = u'\u2028' PS = u'\u2029' U_CATEGORIES = defaultdict(list) -for c in map(unichr, range(sys.maxunicode + 1)): +for c in map(chr, range(sys.maxunicode + 1)): U_CATEGORIES[unicodedata.category(c)].append(c) UNICODE_LETTER = set(U_CATEGORIES['Lu']+U_CATEGORIES['Ll']+ U_CATEGORIES['Lt']+U_CATEGORIES['Lm']+ @@ -242,33 +236,33 @@ WHITE_SPACE = set((0x20, 0x09, 0x0B, 0x0C, 0xA0, 0x1680, LINE_TERMINATORS = set((0x0A, 0x0D, 0x2028, 0x2029)) def isIdentifierStart(ch): - return (ch if isinstance(ch, unicode) else unichr(ch)) in IDENTIFIER_START + return (ch if isinstance(ch, str) else chr(ch)) in IDENTIFIER_START def isIdentifierPart(ch): - return (ch if isinstance(ch, unicode) else unichr(ch)) in IDENTIFIER_PART + return (ch if isinstance(ch, str) else chr(ch)) in IDENTIFIER_PART def isWhiteSpace(ch): - return (ord(ch) if isinstance(ch, unicode) else ch) in WHITE_SPACE + return (ord(ch) if isinstance(ch, str) else ch) in WHITE_SPACE def isLineTerminator(ch): - return (ord(ch) if isinstance(ch, unicode) else ch) in LINE_TERMINATORS + return (ord(ch) if isinstance(ch, str) else ch) in LINE_TERMINATORS OCTAL = set(('0', '1', '2', '3', '4', '5', '6', '7')) DEC = set(('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')) HEX = set('0123456789abcdefABCDEF') -HEX_CONV = dict(('0123456789abcdef'[n],n) for n in xrange(16)) +HEX_CONV = dict(('0123456789abcdef'[n],n) for n in range(16)) for i,e in enumerate('ABCDEF', 10): HEX_CONV[e] = i def isDecimalDigit(ch): - return (ch if isinstance(ch, unicode) else unichr(ch)) in DEC + return (ch if isinstance(ch, str) else chr(ch)) in DEC def isHexDigit(ch): - return (ch if isinstance(ch, unicode) else unichr(ch)) in HEX + return (ch if isinstance(ch, str) else chr(ch)) in HEX def isOctalDigit(ch): - return (ch if isinstance(ch, unicode) else unichr(ch)) in OCTAL + return (ch if isinstance(ch, str) else chr(ch)) in OCTAL def isFutureReservedWord(w): return w in ('enum', 'export', 'import', 'super') diff --git a/doc/python3.rst b/doc/python3.rst index ac24983ceb7..cfa3389a650 100644 --- a/doc/python3.rst +++ b/doc/python3.rst @@ -62,16 +62,6 @@ features whereas: * Python 3.2 made ``range`` views more list-like (backported to 2.7)and reintroduced ``callable`` -.. warning:: - - 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 - -.. note:: - - In the *very rare* cases where you *need* to differentiate between - Python 2 and Python 3, use the :data:`odoo.tools.pycompat.PY2` flag. Semantics changes ================= diff --git a/odoo/addons/base/models/qweb.py b/odoo/addons/base/models/qweb.py index 071967093d4..d8b1a3cdde2 100644 --- a/odoo/addons/base/models/qweb.py +++ b/odoo/addons/base/models/qweb.py @@ -78,10 +78,7 @@ class Contextifier(ast.NodeTransformer): def visit_Lambda(self, node): args = node.args # assume we don't have any tuple parameter, just names - if pycompat.PY2: - names = [arg.id for arg in args.args] - else: - names = [arg.arg for arg in args.args] + names = [arg.arg for arg in args.args] if args.vararg: names.append(args.vararg) if args.kwarg: names.append(args.kwarg) # remap defaults in case there's any diff --git a/odoo/fields.py b/odoo/fields.py index 5a523f28fb9..8da18806f96 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -1735,9 +1735,7 @@ class Datetime(Field): # http://initd.org/psycopg/docs/usage.html#binary-adaptation # Received data is returned as buffer (in Python 2) or memoryview (in Python 3). _BINARY = memoryview -if pycompat.PY2: - #pylint: disable=buffer-builtin,undefined-variable - _BINARY = buffer + class Binary(Field): type = 'binary' diff --git a/odoo/models.py b/odoo/models.py index bbd4a67d1e1..00e44fcacdc 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -193,7 +193,6 @@ LOG_ACCESS_COLUMNS = ['create_uid', 'create_date', 'write_uid', 'write_date'] MAGIC_COLUMNS = ['id'] + LOG_ACCESS_COLUMNS -@pycompat.implements_to_string class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): """ Base class for Odoo models. diff --git a/odoo/modules/migration.py b/odoo/modules/migration.py index 4081232b5ff..2dec12674de 100644 --- a/odoo/modules/migration.py +++ b/odoo/modules/migration.py @@ -5,6 +5,7 @@ from collections import defaultdict import glob +import importlib.util import logging import os from os.path import join as opj @@ -14,40 +15,17 @@ import odoo.release as release import odoo.tools as tools from odoo.tools.parse_version import parse_version -if pycompat.PY2: - import imp - def load_script(path, module_name): - fp, fname = tools.file_open(path, pathinfo=True) - fp2 = None - - # pylint: disable=file-builtin,undefined-variable - if not isinstance(fp, file): - # imp.load_source need a real file object, so we create - # one from the file-like object we get from file_open - fp2 = os.tmpfile() - fp2.write(fp.read()) - fp2.seek(0) - - try: - return imp.load_source(module_name, fname, fp2 or fp) - finally: - if fp: - fp.close() - if fp2: - fp2.close() - -else: - import importlib.util - def load_script(path, module_name): - full_path = get_resource_path(*path.split(os.path.sep)) - spec = importlib.util.spec_from_file_location(module_name, full_path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - _logger = logging.getLogger(__name__) + +def load_script(path, module_name): + full_path = get_resource_path(*path.split(os.path.sep)) + spec = importlib.util.spec_from_file_location(module_name, full_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + class MigrationManager(object): """ This class manage the migration of modules diff --git a/odoo/modules/module.py b/odoo/modules/module.py index 4f7f80ca396..c94df025f9b 100644 --- a/odoo/modules/module.py +++ b/odoo/modules/module.py @@ -434,12 +434,8 @@ def get_test_modules(module): mod = importlib.import_module('.tests', modpath) except ImportError as e: # will also catch subclass ModuleNotFoundError of P3.6 # Hide ImportErrors on `tests` sub-module, but display other exceptions - if pycompat.PY2: - if e.message.startswith('No module named') and e.message.endswith("tests"): - return [] - else: - if e.name == modpath + '.tests' and e.msg.startswith('No module named'): - return [] + if e.name == modpath + '.tests' and e.msg.startswith('No module named'): + return [] _logger.exception('Can not `import %s`.', module) return [] except Exception as e: diff --git a/odoo/tests/common.py b/odoo/tests/common.py index 89a9473e432..9a03f589200 100644 --- a/odoo/tests/common.py +++ b/odoo/tests/common.py @@ -354,10 +354,9 @@ class BaseCase(TreeCase, MetaCase('DummyCase', (object,), {})): doc = self._testMethodDoc return doc and ' '.join(l.strip() for l in doc.splitlines() if not l.isspace()) or None - if not pycompat.PY2: - # turns out this thing may not be quite as useful as we thought... - def assertItemsEqual(self, a, b, msg=None): - self.assertCountEqual(a, b, msg=None) + # turns out this thing may not be quite as useful as we thought... + def assertItemsEqual(self, a, b, msg=None): + self.assertCountEqual(a, b, msg=None) class TransactionCase(BaseCase): diff --git a/odoo/tools/float_utils.py b/odoo/tools/float_utils.py index c5c2946a5ce..12881503f32 100644 --- a/odoo/tools/float_utils.py +++ b/odoo/tools/float_utils.py @@ -2,28 +2,25 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from __future__ import print_function +import builtins import math -if not pycompat.PY2: - import builtins - 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) -else: - round = round +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) def _float_check_precision(precision_digits=None, precision_rounding=None): assert (precision_digits is not None or precision_rounding is not None) and \ diff --git a/odoo/tools/mail.py b/odoo/tools/mail.py index 847fcfcf2f3..8bce7e056a0 100644 --- a/odoo/tools/mail.py +++ b/odoo/tools/mail.py @@ -545,10 +545,7 @@ def decode_smtp_header(smtp_header): smtp_header = ustr(smtp_header) if smtp_header: text = decode_header(smtp_header.replace('\r', '')) - # The joining space will not be needed as of Python 3.3 - # See https://github.com/python/cpython/commit/07ea53cb218812404cdbde820647ce6e4b2d0f8e - sep = ' ' if pycompat.PY2 else '' - return sep.join([ustr(x[0], x[1]) for x in text]) + return ''.join([ustr(x[0], x[1]) for x in text]) return u'' # was mail_thread.decode_header() diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py index b165eb3a130..e1a16b574a3 100644 --- a/odoo/tools/misc.py +++ b/odoo/tools/misc.py @@ -1002,7 +1002,6 @@ class Collector(Mapping): return len(self._map) -@pycompat.implements_to_string class StackMap(MutableMapping): """ A stack of mappings behaving as a single mapping, and used to implement nested scopes. The lookups search the stack from top to bottom, and diff --git a/odoo/tools/pycompat.py b/odoo/tools/pycompat.py index 5f25e23e977..d13e706b934 100644 --- a/odoo/tools/pycompat.py +++ b/odoo/tools/pycompat.py @@ -6,101 +6,34 @@ import csv import codecs import collections import io -import sys -PY2 = sys.version_info[0] == 2 +def reraise(tp, value, tb=None): + if value.__traceback__ != tb: + raise value.with_traceback(tb) + raise value -_Writer = collections.namedtuple('_Writer', 'writerow writerows') -if PY2: - # pylint: disable=long-builtin,unichr-builtin,unicode-builtin,undefined-variable - unichr = unichr - text_type = unicode - string_types = (str, unicode) - def to_native(source, encoding='utf-8', falsy_empty=False): - if not source and falsy_empty: - return '' - if isinstance(source, text_type): - return source.encode(encoding) +_reader = codecs.getreader('utf-8') +_writer = codecs.getwriter('utf-8') - return str(source) - integer_types = (int, long) +def csv_reader(stream, **params): + assert not isinstance(stream, io.TextIOBase),\ + "For cross-compatibility purposes, csv_reader takes a bytes stream" + return csv.reader(_reader(stream), **params) - # noinspection PyUnresolvedReferences - from itertools import imap, izip, ifilter - def implements_to_string(cls): - cls.__unicode__ = cls.__str__ - cls.__str__ = lambda x: x.__unicode__().encode('utf-8') - return cls +def csv_writer(stream, **params): + assert not isinstance(stream, io.TextIOBase), \ + "For cross-compatibility purposes, csv_writer takes a bytes stream" + return csv.writer(_writer(stream), **params) - def implements_iterator(cls): - cls.next = cls.__next__ - del cls.__next__ - return cls - - exec ('def reraise(tp, value, tb=None):\n raise tp, value, tb') - - def csv_reader(stream, **params): - for row in csv.reader(stream, **params): - yield [c.decode('utf-8') for c in row] - def csv_writer(stream, **params): - w = csv.writer(stream, **params) - return _Writer( - writerow=lambda r: w.writerow([c.encode('utf-8') for c in r]), - writerows=lambda rs: w.writerows( - [c.encode('utf-8') for c in r] - for r in rs - ) - ) -else: - # pylint: disable=bad-functions - unichr = chr - text_type = str - string_types = (str,) - def to_native(source, encoding='utf-8', falsy_empty=False): - if not source and falsy_empty: - return '' - - if isinstance(source, bytes): - return source.decode(encoding) - - return str(source) - - integer_types = (int,) - - imap = map - izip = zip - ifilter = filter - - def implements_to_string(cls): - return cls - - def implements_iterator(cls): - return cls - - def reraise(tp, value, tb=None): - if value.__traceback__ != tb: - raise value.with_traceback(tb) - raise value - - _reader = codecs.getreader('utf-8') - _writer = codecs.getwriter('utf-8') - def csv_reader(stream, **params): - assert not isinstance(stream, io.TextIOBase),\ - "For cross-compatibility purposes, csv_reader takes a bytes stream" - return csv.reader(_reader(stream), **params) - def csv_writer(stream, **params): - assert not isinstance(stream, io.TextIOBase), \ - "For cross-compatibility purposes, csv_writer takes a bytes stream" - return csv.writer(_writer(stream), **params) def to_text(source): - """ Generates a text value (an instance of text_type) from an arbitrary + """ Generates a text value (an instance of text_type) from an arbitrary source. - + * False and None are converted to empty strings * text is passed through * bytes are decoded as UTF-8 @@ -112,4 +45,4 @@ def to_text(source): if isinstance(source, bytes): return source.decode('utf-8') - return text_type(source) + return str(source)