diff --git a/.drone.yml b/.drone.yml index 9d7c769d8c..c4fd423e94 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,9 +1,9 @@ -image: python2.7 +image: python:all env: - POSTGRESQL_URI=postgresql://postgres@127.0.0.1:5432/ - MYSQL_URI=mysql://root@127.0.0.1:3306/ script: - pip install tox - - tox -e "py27-{sqlite,postgresql}" + - tox -e "{py27,py33,py34,py35}-{sqlite,postgresql}" --skip-missing-interpreters services: - postgres diff --git a/CHANGELOG b/CHANGELOG index 4a281d831d..22a7a8f830 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add Python3 support * Make TestCase create and drop its database * Add with_transaction decorator for tests * Add note on resources diff --git a/setup.py b/setup.py index c51c3a348a..4efa071aca 100644 --- a/setup.py +++ b/setup.py @@ -5,11 +5,14 @@ from setuptools import setup, find_packages import os import re +import io import platform def read(fname): - return open(os.path.join(os.path.dirname(__file__), fname)).read() + return io.open( + os.path.join(os.path.dirname(__file__), fname), + 'r', encoding='utf-8').read() def get_version(): @@ -78,6 +81,9 @@ setup(name=name, 'Natural Language :: Spanish', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries :: Application Frameworks', @@ -106,4 +112,5 @@ setup(name=name, test_suite='trytond.tests', test_loader='trytond.test_loader:Loader', tests_require=['mock'], + use_2to3=True, ) diff --git a/tox.ini b/tox.ini index f8e91e9937..eee8cfdb68 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,11 @@ [tox] -envlist = py27-{sqlite,postgresql,mysql},pypy-{sqlite,postgresql} +envlist = {py27,py33,py34,py35}-{sqlite,postgresql,mysql},pypy-{sqlite,postgresql} [testenv] commands = {envpython} setup.py test deps = mock - py27-postgresql: psycopg2 >= 2.0 + {py27,py33,py34,py35}-postgresql: psycopg2 >= 2.0 pypy-postgresql: psycopg2cffi >= 2.5 mysql: MySQL-python setenv = diff --git a/trytond/backend/postgresql/database.py b/trytond/backend/postgresql/database.py index 7a8675c3ba..9d2fa11c2d 100644 --- a/trytond/backend/postgresql/database.py +++ b/trytond/backend/postgresql/database.py @@ -216,7 +216,6 @@ class Database(DatabaseInterface): 'WHERE datistemplate = false ORDER BY datname') res = [] for db_name, in cursor.fetchall(): - db_name = db_name.encode('utf-8') try: database = Database(db_name).connect() except Exception: diff --git a/trytond/backend/postgresql/table.py b/trytond/backend/postgresql/table.py index 66407f5f7b..10e62d7e7d 100644 --- a/trytond/backend/postgresql/table.py +++ b/trytond/backend/postgresql/table.py @@ -260,11 +260,11 @@ class TableHandler(TableHandlerInterface): and self._columns[column_name]['typname'] == 'varchar'): # Migrate size if field_size is None: - if self._columns[column_name]['size'] > 0: + if self._columns[column_name]['size']: self.alter_size(column_name, base_type) elif self._columns[column_name]['size'] == field_size: pass - elif (self._columns[column_name]['size'] > 0 + elif (self._columns[column_name]['size'] and self._columns[column_name]['size'] < field_size): self.alter_size(column_name, column_type[1]) else: diff --git a/trytond/ir/lang.py b/trytond/ir/lang.py index a6f8c24aa6..47d52dfdaf 100644 --- a/trytond/ir/lang.py +++ b/trytond/ir/lang.py @@ -1,7 +1,13 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. +from __future__ import absolute_import + +import sys import datetime import warnings +warnings.filterwarnings('ignore', "", ImportWarning) +from locale import CHAR_MAX +warnings.resetwarnings() from ast import literal_eval from ..model import ModelView, ModelSQL, fields, Check @@ -12,10 +18,6 @@ from ..pool import Pool from .time_locale import TIME_LOCALE from ..backend.database import CursorInterface -warnings.filterwarnings('ignore', "", ImportWarning) -from locale import CHAR_MAX -warnings.resetwarnings() - CursorInterface.cache_keys.add('translate_name') __all__ = [ @@ -154,9 +156,11 @@ class Lang(ModelSQL, ModelView): Check the date format ''' for lang in langs: + date = lang.date + if sys.version_info < (3,): + date = date.encode('utf-8') try: - datetime_strftime(datetime.datetime.now(), - lang.date.encode('utf-8')) + datetime_strftime(datetime.datetime.now(), date) except Exception: cls.raise_user_error('invalid_date', { 'format': lang.date, @@ -415,7 +419,10 @@ class Lang(ModelSQL, ModelView): TIME_LOCALE[code][f][datetime.timetuple()[i]]) format = format.replace('%p', TIME_LOCALE[code]['%p'][datetime.timetuple()[3] < 12 and 0 - or 1]).encode('utf-8') - else: - format = format.encode('utf-8') - return datetime_strftime(datetime, format).decode('utf-8') + or 1]) + if sys.version_info < (3,): + format.encode('utf-8') + result = datetime_strftime(datetime, format) + if sys.version_info < (3,): + result = result.decode('utf-8') + return result diff --git a/trytond/ir/translation.py b/trytond/ir/translation.py index 4e3f69fff7..675145d5a9 100644 --- a/trytond/ir/translation.py +++ b/trytond/ir/translation.py @@ -1,9 +1,5 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. -try: - import cStringIO as StringIO -except ImportError: - import StringIO import zipfile import polib import xml.dom.minidom @@ -12,6 +8,7 @@ import os from hashlib import md5 from lxml import etree from itertools import izip +from io import BytesIO from sql import Column, Null from sql.functions import Substring, Position @@ -438,7 +435,7 @@ class Translation(ModelSQL, ModelView): fuzzy_sql = table.fuzzy == False if Transaction().context.get('fuzzy_translation', False): fuzzy_sql = None - in_max = cursor.IN_MAX / 7 + in_max = cursor.IN_MAX // 7 for sub_to_fetch in grouped_slice(to_fetch, in_max): red_sql = reduce_ids(table.res_id, sub_to_fetch) where = And(((table.lang == lang), @@ -673,7 +670,7 @@ class Translation(ModelSQL, ModelView): where &= table.src == source clause.append(where) if clause: - in_max = cursor.IN_MAX / 7 + in_max = cursor.IN_MAX // 7 for sub_clause in grouped_slice(clause, in_max): cursor.execute(*table.select( table.lang, table.type, table.name, table.src, @@ -1041,7 +1038,7 @@ class TranslationSet(Wizard): if not content: continue - content_io = StringIO.StringIO(content) + content_io = BytesIO(content) try: content_z = zipfile.ZipFile(content_io, mode='r') except zipfile.BadZipfile: diff --git a/trytond/ir/ui/view.py b/trytond/ir/ui/view.py index ac19d8b37f..24cfc3ac95 100644 --- a/trytond/ir/ui/view.py +++ b/trytond/ir/ui/view.py @@ -121,10 +121,13 @@ class View(ModelSQL, ModelView): key = (cls.__name__, type_) rng = cls._get_rng_cache.get(key) if rng is None: - rng_name = os.path.join(os.path.dirname( - unicode(__file__, sys.getfilesystemencoding())), - type_ + '.rng') - rng = etree.fromstring(open(rng_name).read()) + if sys.version_info < (3,): + filename = __file__.decode(sys.getfilesystemencoding()) + else: + filename = __file__ + rng_name = os.path.join(os.path.dirname(filename), type_ + '.rng') + with open(rng_name, 'rb') as fp: + rng = etree.fromstring(fp.read()) cls._get_rng_cache.set(key, rng) return rng diff --git a/trytond/model/fields/char.py b/trytond/model/fields/char.py index d2d6b611c4..83ba32370b 100644 --- a/trytond/model/fields/char.py +++ b/trytond/model/fields/char.py @@ -1,5 +1,6 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. +import sys import warnings from sql import Query, Expression @@ -53,7 +54,7 @@ class Char(FieldTranslate): return value if value is None: return None - elif isinstance(value, str): + elif isinstance(value, str) and sys.version_info < (3,): return unicode(value, 'utf-8') assert isinstance(value, unicode) return value diff --git a/trytond/model/fields/many2one.py b/trytond/model/fields/many2one.py index 3730dece72..7ed55598dd 100644 --- a/trytond/model/fields/many2one.py +++ b/trytond/model/fields/many2one.py @@ -1,6 +1,5 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. -from types import NoneType from sql import Query, Expression, Literal from sql.operators import Or @@ -76,7 +75,7 @@ class Many2One(Field): value = Target(**value) elif isinstance(value, (int, long)): value = Target(value) - assert isinstance(value, (Target, NoneType)) + assert isinstance(value, (Target, type(None))) super(Many2One, self).__set__(inst, value) @staticmethod diff --git a/trytond/model/fields/one2one.py b/trytond/model/fields/one2one.py index 60139dca5a..3b9e322f5a 100644 --- a/trytond/model/fields/one2one.py +++ b/trytond/model/fields/one2one.py @@ -1,6 +1,5 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. -from types import NoneType from trytond.model.fields.field import Field from trytond.model.fields.many2many import Many2Many @@ -59,5 +58,5 @@ class One2One(Many2Many): value = Target(*value) elif isinstance(value, (int, long)): value = Target(value) - assert isinstance(value, (Target, NoneType)) + assert isinstance(value, (Target, type(None))) Field.__set__(self, inst, value) diff --git a/trytond/model/fields/reference.py b/trytond/model/fields/reference.py index 970351ca41..8489a8ad6d 100644 --- a/trytond/model/fields/reference.py +++ b/trytond/model/fields/reference.py @@ -1,6 +1,5 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. -from types import NoneType import warnings from sql import Cast, Literal, Query, Expression @@ -93,7 +92,7 @@ class Reference(Field): def __set__(self, inst, value): from ..model import Model - if not isinstance(value, (Model, NoneType)): + if not isinstance(value, (Model, type(None))): if isinstance(value, basestring): target, value = value.split(',') else: diff --git a/trytond/model/modelsql.py b/trytond/model/modelsql.py index 59917b5b50..284cd244eb 100644 --- a/trytond/model/modelsql.py +++ b/trytond/model/modelsql.py @@ -298,10 +298,10 @@ class ModelSQL(ModelStorage): cls.raise_user_error('foreign_model_missing', error_args=error_args) for name, _, error in cls._sql_constraints: - if name in str(exception[0]): + if name in unicode(exception): cls.raise_user_error(error) for name, error in cls._sql_error_messages.iteritems(): - if name in str(exception[0]): + if name in unicode(exception): cls.raise_user_error(error) @classmethod @@ -1411,7 +1411,7 @@ class ModelSQL(ModelStorage): if isinstance(sql, Unique): columns = [Column(table, c.name) for c in sql.columns] columns.insert(0, table.id) - in_max = cursor.IN_MAX / (len(columns) + 1) + in_max = cursor.IN_MAX // (len(columns) + 1) for sub_ids in grouped_slice(ids, in_max): red_sql = reduce_ids(table.id, sub_ids) diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py index 0d447dceac..313941423e 100644 --- a/trytond/model/modelstorage.py +++ b/trytond/model/modelstorage.py @@ -5,10 +5,6 @@ import datetime import time import csv import warnings -try: - import cStringIO as StringIO -except ImportError: - import StringIO from decimal import Decimal from itertools import islice, ifilter, chain, izip @@ -569,7 +565,7 @@ class ModelStorage(Model): return None res = [] Relation = pool.get(relation) - for word in csv.reader(StringIO.StringIO(value), delimiter=',', + for word in csv.reader(value.splitlines(), delimiter=',', quoting=csv.QUOTE_NONE, escapechar='\\').next(): res2 = Relation.search([ ('rec_name', '=', word), @@ -618,7 +614,7 @@ class ModelStorage(Model): relation = None ftype = fields_def[field[-1][:-3]]['type'] if ftype == 'many2many': - value = csv.reader(StringIO.StringIO(value), delimiter=',', + value = csv.reader(value.splitlines(), delimiter=',', quoting=csv.QUOTE_NONE, escapechar='\\').next() elif ftype == 'reference': try: @@ -1191,7 +1187,7 @@ class ModelStorage(Model): try: return super(ModelStorage, self).__getattr__(name) except AttributeError: - if self.id < 0: + if self.id is None or self.id < 0: raise counter = Transaction().counter @@ -1373,7 +1369,8 @@ class ModelStorage(Model): field = self._fields[fname] if field._type in ('many2one', 'one2one', 'reference'): if value: - if value.id < 0 and field._type != 'reference': + if ((value.id is None or value.id < 0) + and field._type != 'reference'): value.save() if field._type == 'reference': value = str(value) @@ -1381,7 +1378,7 @@ class ModelStorage(Model): value = value.id if field._type in ('one2many', 'many2many'): targets = value - if self.id >= 0: + if self.id is not None and self.id >= 0: _values, self._values = self._values, None try: to_remove = [t.id for t in getattr(self, fname)] @@ -1393,7 +1390,7 @@ class ModelStorage(Model): to_create = [] to_write = [] for target in targets: - if target.id < 0: + if target.id is None or target.id < 0: if field._type == 'one2many': # Don't store old target link setattr(target, field.field, None) diff --git a/trytond/model/modelview.py b/trytond/model/modelview.py index d7ba1aea7f..1c4b278215 100644 --- a/trytond/model/modelview.py +++ b/trytond/model/modelview.py @@ -303,7 +303,8 @@ class ModelView(Model): continue tree_inherit = etree.fromstring(view.arch, parser=parser) tree = _inherit_apply(tree, tree_inherit) - result['arch'] = etree.tostring(tree, encoding='utf-8') + result['arch'] = etree.tostring( + tree, encoding='utf-8').decode('utf-8') # otherwise, build some kind of default view else: @@ -465,7 +466,8 @@ class ModelView(Model): if 'active' in cls._fields: fields_def.setdefault('active', {'name': 'active'}) - arch = etree.tostring(tree, encoding='utf-8', pretty_print=False) + arch = etree.tostring( + tree, encoding='utf-8', pretty_print=False).decode('utf-8') fields2 = cls.fields_get(fields_def.keys()) for field in fields_def: if field in fields2: diff --git a/trytond/protocols/jsonrpc.py b/trytond/protocols/jsonrpc.py index df9f46e697..edaa35571c 100644 --- a/trytond/protocols/jsonrpc.py +++ b/trytond/protocols/jsonrpc.py @@ -6,6 +6,7 @@ from trytond.config import config from trytond.protocols.common import daemon, RegisterHandlerMixin from trytond.exceptions import UserError, UserWarning, NotLogged, \ ConcurrencyException + import SimpleXMLRPCServer import SimpleHTTPServer import SocketServer @@ -27,10 +28,7 @@ except ImportError: import json import base64 import encodings -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO +from io import BytesIO class JSONDecoder(object): @@ -61,7 +59,7 @@ JSONDecoder.register('timedelta', def _bytes_decoder(dct): cast = bytearray if bytes == str else bytes - return cast(base64.decodestring(dct['base64'])) + return cast(base64.decodestring(dct['base64'].encode('utf-8'))) JSONDecoder.register('bytes', _bytes_decoder) JSONDecoder.register('Decimal', lambda dct: Decimal(dct['decimal'])) @@ -118,7 +116,7 @@ JSONEncoder.register(datetime.timedelta, }) _bytes_encoder = lambda o: { '__class__': 'bytes', - 'base64': base64.encodestring(o), + 'base64': base64.encodestring(o).decode('utf-8'), } JSONEncoder.register(bytes, _bytes_encoder) JSONEncoder.register(bytearray, _bytes_encoder) @@ -148,7 +146,7 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher): existing method through subclassing is the prefered means of changing method dispatch behavior. """ - rawreq = json.loads(data, object_hook=JSONDecoder()) + rawreq = json.loads(data.decode('utf-8'), object_hook=JSONDecoder()) req_id = rawreq.get('id', 0) method = rawreq['method'] @@ -172,7 +170,7 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher): # report exception back to server response['error'] = (str(sys.exc_value), tb_s) - return json.dumps(response, cls=JSONEncoder) + return json.dumps(response, cls=JSONEncoder).encode('utf-8') class GenericJSONRPCRequestHandler: @@ -263,20 +261,23 @@ class SimpleJSONRPCRequestHandler(RegisterHandlerMixin, 'hostname': hostname, 'path': path, } - content = StringIO() - content.write('') - content.write('') + write('' % values) - content.write('
This page has moved to ' + write('
This page has moved to ' '' 'tryton://%(hostname)s%(path)s.
' % values) - content.write('') - content.write('