diff --git a/.travis.yml b/.travis.yml index f879f6b34..4bd76051e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,14 @@ language: python python: - - "2.6" - "2.7" + - "3.3" env: - - DJANGO=1.4 + - DJANGO=1.4.10 + - DJANGO=1.5.5 + - DJANGO=1.6.1 install: - pip install -q Django==$DJANGO --use-mirrors + - pip install -e git+https://github.com/kennethreitz/tablib.git#egg=tablib - pip install -r requirements/base.txt --use-mirrors script: - - python tests/manage.py test core --settings=settings \ No newline at end of file + - if [[ $TRAVIS_PYTHON_VERSION != '3.3' && $DJANGO != "1.4.10" ]]; then python tests/manage.py test core --settings=settings; fi diff --git a/README.rst b/README.rst index 399f32878..7e48b9555 100644 --- a/README.rst +++ b/README.rst @@ -43,3 +43,10 @@ Username and password for admin are 'admin', 'password'. .. _`tablib`: https://github.com/kennethreitz/tablib + +Requirements +============ + +* Python 2.7+ or Python 3.3+ +* Django 1.4.2+ +* tablib (dev or 0.9.11) diff --git a/docs/changelog.rst b/docs/changelog.rst index da1c424e2..844b69a19 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,10 +1,10 @@ Changelog for django-import-export ================================== -0.1.7 (unreleased) +0.2.0 (unreleased) ------------------ -- Nothing changed yet. +- Python 3 support 0.1.6 (2014-01-21) diff --git a/import_export/__init__.py b/import_export/__init__.py index 80c80ae21..71ae8f5cb 100644 --- a/import_export/__init__.py +++ b/import_export/__init__.py @@ -1 +1 @@ -__version__ = "0.1.7.dev0" +__version__ = "0.2.1.dev0" diff --git a/import_export/admin.py b/import_export/admin.py index 61c038666..7456b90ed 100644 --- a/import_export/admin.py +++ b/import_export/admin.py @@ -21,6 +21,11 @@ ) from .formats import base_formats +try: + from django.utils.encoding import force_text +except ImportError: + from django.utils.encoding import force_unicode as force_text + #: import / export formats DEFAULT_FORMATS = ( @@ -100,7 +105,7 @@ def process_import(self, request, *args, **kwargs): input_format.get_read_mode()) data = import_file.read() if not input_format.is_binary() and self.from_encoding: - data = unicode(data, self.from_encoding).encode('utf-8') + data = force_text(data, self.from_encoding) dataset = input_format.create_dataset(data) resource.import_data(dataset, dry_run=False, @@ -148,7 +153,7 @@ def import_action(self, request, *args, **kwargs): # warning, big files may exceed memory data = uploaded_import_file.read() if not input_format.is_binary() and self.from_encoding: - data = unicode(data, self.from_encoding).encode('utf-8') + data = force_text(data, self.from_encoding) dataset = input_format.create_dataset(data) result = resource.import_data(dataset, dry_run=True, raise_errors=False) diff --git a/import_export/exceptions.py b/import_export/exceptions.py index 94a992e0c..e129cf011 100644 --- a/import_export/exceptions.py +++ b/import_export/exceptions.py @@ -1,3 +1,6 @@ +from __future__ import unicode_literals + + class ImportExportError(Exception): """A generic exception for all others to extend.""" pass diff --git a/import_export/fields.py b/import_export/fields.py index cffca2107..6031a6202 100644 --- a/import_export/fields.py +++ b/import_export/fields.py @@ -1,7 +1,10 @@ +from __future__ import unicode_literals + from . import widgets from django.core.exceptions import ObjectDoesNotExist + class Field(object): """ Field represent mapping between `object` field and representation of diff --git a/import_export/formats/__init__.py b/import_export/formats/__init__.py index e69de29bb..baffc4882 100644 --- a/import_export/formats/__init__.py +++ b/import_export/formats/__init__.py @@ -0,0 +1 @@ +from __future__ import unicode_literals diff --git a/import_export/formats/base_formats.py b/import_export/formats/base_formats.py index 8819783e4..6d66ffffe 100644 --- a/import_export/formats/base_formats.py +++ b/import_export/formats/base_formats.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import warnings import tablib @@ -15,6 +17,7 @@ XLS_IMPORT = False from django.utils.importlib import import_module +from django.utils import six class Format(object): @@ -103,9 +106,18 @@ def is_binary(self): return False -class CSV(TextFormat): +class CSV(TablibFormat): + """ + CSV is treated as binary in Python 2. + """ TABLIB_MODULE = 'tablib.formats._csv' + def get_read_mode(self): + return 'rU' if six.PY3 else 'rb' + + def is_binary(self): + return False if six.PY3 else True + class JSON(TextFormat): TABLIB_MODULE = 'tablib.formats._json' diff --git a/import_export/forms.py b/import_export/forms.py index 494a77644..099cdedc6 100644 --- a/import_export/forms.py +++ b/import_export/forms.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django import forms from django.utils.translation import ugettext_lazy as _ diff --git a/import_export/instance_loaders.py b/import_export/instance_loaders.py index 952ee67d1..c60d02eea 100644 --- a/import_export/instance_loaders.py +++ b/import_export/instance_loaders.py @@ -1,3 +1,6 @@ +from __future__ import unicode_literals + + class BaseInstanceLoader(object): """ Base abstract implementation of instance loader. diff --git a/import_export/models.py b/import_export/models.py index e69de29bb..baffc4882 100644 --- a/import_export/models.py +++ b/import_export/models.py @@ -0,0 +1 @@ +from __future__ import unicode_literals diff --git a/import_export/resources.py b/import_export/resources.py index b78642b03..54ef97fcc 100644 --- a/import_export/resources.py +++ b/import_export/resources.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import functools from copy import deepcopy import sys @@ -8,6 +10,7 @@ from django.utils.safestring import mark_safe from django.utils.datastructures import SortedDict +from django.utils import six from django.db import transaction from django.db.models.related import RelatedObject from django.conf import settings @@ -16,8 +19,14 @@ from .fields import Field from import_export import widgets from .instance_loaders import ( - ModelInstanceLoader, - ) + ModelInstanceLoader, +) + + +try: + from django.utils.encoding import force_text +except ImportError: + from django.utils.encoding import force_unicode as force_text USE_TRANSACTIONS = getattr(settings, 'IMPORT_EXPORT_USE_TRANSACTIONS', False) @@ -50,7 +59,7 @@ class ResourceOptions(object): * ``use_transactions`` - Controls if import should use database transactions. Default value is ``None`` meaning ``settings.IMPORT_EXPORT_USE_TRANSACTIONS`` will be evaluated. - + * ``skip_unchanged`` - Controls if the import should skip unchanged records. Default value is False @@ -77,7 +86,7 @@ def __new__(cls, meta=None): if not override_name.startswith('_'): overrides[override_name] = getattr(meta, override_name) - return object.__new__(type('ResourceOptions', (cls,), overrides)) + return object.__new__(type(str('ResourceOptions'), (cls,), overrides)) class DeclarativeMetaclass(type): @@ -85,7 +94,7 @@ class DeclarativeMetaclass(type): def __new__(cls, name, bases, attrs): declared_fields = [] - for field_name, obj in attrs.items(): + for field_name, obj in attrs.copy().items(): if isinstance(obj, Field): field = attrs.pop(field_name) if not field.column_name: @@ -101,12 +110,11 @@ def __new__(cls, name, bases, attrs): return new_class -class Resource(object): +class Resource(six.with_metaclass(DeclarativeMetaclass)): """ Resource defines how objects are mapped to their import and export representations and handle importing and exporting data. """ - __metaclass__ = DeclarativeMetaclass def get_use_transactions(self): if self._meta.use_transactions is None: @@ -247,7 +255,7 @@ def get_diff(self, original, current, dry_run=False): for field in self.get_fields(): v1 = self.export_field(field, original) if original else "" v2 = self.export_field(field, current) if current else "" - diff = dmp.diff_main(unicode(v1), unicode(v2)) + diff = dmp.diff_main(force_text(v1), force_text(v2)) dmp.diff_cleanupSemantic(diff) html = dmp.diff_prettyHtml(diff) html = mark_safe(html) @@ -294,7 +302,7 @@ def import_data(self, dataset, dry_run=False, raise_errors=False, try: self.before_import(dataset, real_dry_run) - except Exception, e: + except Exception as e: tb_info = traceback.format_exc(sys.exc_info()[2]) result.base_errors.append(Error(repr(e), tb_info)) if raise_errors: @@ -332,16 +340,16 @@ def import_data(self, dataset, dry_run=False, raise_errors=False, self.save_m2m(instance, row, real_dry_run) row_result.diff = self.get_diff(original, instance, real_dry_run) - except Exception, e: - tb_info = traceback.format_exc(sys.exc_info()[2]) + except Exception as e: + tb_info = traceback.format_exc(2) row_result.errors.append(Error(repr(e), tb_info)) if raise_errors: if use_transactions: transaction.rollback() transaction.leave_transaction_management() - raise - if (row_result.import_type != RowResult.IMPORT_TYPE_SKIP or - self._meta.report_skipped): + six.reraise(*sys.exc_info()) + if (row_result.import_type != RowResult.IMPORT_TYPE_SKIP or + self._meta.report_skipped): result.rows.append(row_result) if use_transactions: @@ -446,11 +454,10 @@ def __new__(cls, name, bases, attrs): return new_class -class ModelResource(Resource): +class ModelResource(six.with_metaclass(ModelDeclarativeMetaclass, Resource)): """ ModelResource is Resource subclass for handling Django models. """ - __metaclass__ = ModelDeclarativeMetaclass @classmethod def widget_from_django_field(cls, f, default=widgets.Widget): @@ -503,9 +510,9 @@ def modelresource_factory(model, resource_class=ModelResource): Factory for creating ``ModelResource`` class for given Django model. """ attrs = {'model': model} - Meta = type('Meta', (object,), attrs) + Meta = type(str('Meta'), (object,), attrs) - class_name = model.__name__ + 'Resource' + class_name = model.__name__ + str('Resource') class_attrs = { 'Meta': Meta, diff --git a/import_export/results.py b/import_export/results.py index c080350b9..d7c0137fe 100644 --- a/import_export/results.py +++ b/import_export/results.py @@ -1,3 +1,6 @@ +from __future__ import unicode_literals + + class Error(object): def __init__(self, error, traceback=None): diff --git a/import_export/widgets.py b/import_export/widgets.py index 339aa11aa..65b9aedcb 100644 --- a/import_export/widgets.py +++ b/import_export/widgets.py @@ -1,6 +1,13 @@ +from __future__ import unicode_literals + from decimal import Decimal from datetime import datetime +try: + from django.utils.encoding import force_text +except ImportError: + from django.utils.encoding import force_unicode as force_text + class Widget(object): """ @@ -23,7 +30,7 @@ def render(self, value): """ Returns export representation of python value. """ - return unicode(value) + return force_text(value) class IntegerWidget(Widget): @@ -54,7 +61,7 @@ class CharWidget(Widget): """ def render(self, value): - return unicode(value) + return force_text(value) class BooleanWidget(Widget): diff --git a/setup.py b/setup.py index 697ca71a3..886c4b0eb 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,9 @@ 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Topic :: Software Development', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3', ] install_requires = [ diff --git a/tests/core/admin.py b/tests/core/admin.py index 99d2d7c7f..66ba976c1 100644 --- a/tests/core/admin.py +++ b/tests/core/admin.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib import admin from import_export.admin import ImportExportMixin diff --git a/tests/core/models.py b/tests/core/models.py index 3101d7ebb..4fe6d4a09 100644 --- a/tests/core/models.py +++ b/tests/core/models.py @@ -1,21 +1,27 @@ +from __future__ import unicode_literals + from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Author(models.Model): name = models.CharField(max_length=100) birthday = models.DateTimeField(auto_now_add=True) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Category(models.Model): name = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Book(models.Model): name = models.CharField('Book name', max_length=100) author = models.ForeignKey(Author, blank=True, null=True) @@ -26,7 +32,7 @@ class Book(models.Model): blank=True) categories = models.ManyToManyField(Category, blank=True) - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/core/tests/admin_integration_tests.py b/tests/core/tests/admin_integration_tests.py index d2b5d2883..14c35ef23 100644 --- a/tests/core/tests/admin_integration_tests.py +++ b/tests/core/tests/admin_integration_tests.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import os.path from django.test.testcases import TestCase @@ -28,15 +30,16 @@ def test_import_export_template(self): def test_import(self): input_format = '0' filename = os.path.join( - os.path.dirname(__file__), - os.path.pardir, - 'exports', - 'books.csv') - data = { + os.path.dirname(__file__), + os.path.pardir, + 'exports', + 'books.csv') + with open(filename, "rb") as f: + data = { 'input_format': input_format, - 'import_file': open(filename), - } - response = self.client.post('/admin/core/book/import/', data) + 'import_file': f, + } + response = self.client.post('/admin/core/book/import/', data) self.assertEqual(response.status_code, 200) self.assertIn('result', response.context) self.assertFalse(response.context['result'].has_errors()) diff --git a/tests/core/tests/base_formats_tests.py b/tests/core/tests/base_formats_tests.py index 4c9d86228..b6543db57 100644 --- a/tests/core/tests/base_formats_tests.py +++ b/tests/core/tests/base_formats_tests.py @@ -1,4 +1,7 @@ +from __future__ import unicode_literals + from django.test import TestCase +from django.utils import six from import_export.formats import base_formats @@ -12,4 +15,4 @@ def test_binary_format(self): class CSVTest(TestCase): def test_binary_format(self): - self.assertFalse(base_formats.CSV().is_binary()) + self.assertEqual(base_formats.CSV().is_binary(), not six.PY3) diff --git a/tests/core/tests/fields_tests.py b/tests/core/tests/fields_tests.py index 658bc69a7..5c30d8c6c 100644 --- a/tests/core/tests/fields_tests.py +++ b/tests/core/tests/fields_tests.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from datetime import date from django.test import TestCase diff --git a/tests/core/tests/instance_loaders_tests.py b/tests/core/tests/instance_loaders_tests.py index c956f9b8d..1a5346364 100644 --- a/tests/core/tests/instance_loaders_tests.py +++ b/tests/core/tests/instance_loaders_tests.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import tablib from django.test import TestCase @@ -5,7 +7,7 @@ from import_export import instance_loaders from import_export import resources -from ..models import Book +from core.models import Book class CachedInstanceLoaderTest(TestCase): @@ -23,7 +25,7 @@ def setUp(self): def test_all_instances(self): self.assertTrue(self.instance_loader.all_instances) self.assertEqual(len(self.instance_loader.all_instances), 1) - self.assertEqual(self.instance_loader.all_instances.keys(), + self.assertEqual(list(self.instance_loader.all_instances.keys()), [self.book.pk]) def test_get_instance(self): diff --git a/tests/core/tests/resources_tests.py b/tests/core/tests/resources_tests.py index df7ed6e6e..4faa7e93b 100644 --- a/tests/core/tests/resources_tests.py +++ b/tests/core/tests/resources_tests.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from decimal import Decimal from datetime import date from copy import deepcopy @@ -18,7 +20,12 @@ from import_export import results from import_export.instance_loaders import ModelInstanceLoader -from ..models import Book, Author, Category, Entry, Profile +from core.models import Book, Author, Category, Entry, Profile + +try: + from django.utils.encoding import force_text +except ImportError: + from django.utils.encoding import force_unicode as force_text class MyResource(resources.Resource): @@ -293,7 +300,7 @@ def test_empty_get_queryset(self): self.assertEqual(len(dataset), 0) def test_import_data_skip_unchanged(self): - def attempted_save(instance, real_dry_run): + def attempted_save(instance, real_dry_run): self.fail('Resource attempted to save instead of skipping') # Make sure we test with ManyToMany related objects @@ -308,12 +315,12 @@ def attempted_save(instance, real_dry_run): resource = deepcopy(self.resource) resource._meta.skip_unchanged = True # Fail the test if the resource attempts to save the row - resource.save_instance = attempted_save + resource.save_instance = attempted_save result = resource.import_data(dataset, raise_errors=True) self.assertFalse(result.has_errors()) self.assertEqual(len(result.rows), len(dataset)) self.assertTrue(result.rows[0].diff) - self.assertEqual(result.rows[0].import_type, + self.assertEqual(result.rows[0].import_type, results.RowResult.IMPORT_TYPE_SKIP) # Test that we can suppress reporting of skipped rows @@ -348,7 +355,7 @@ def test_m2m_import_with_transactions(self): category_field = self.resource.fields['categories'] categories_diff = row_diff[fields.index(category_field)] - self.assertEqual(strip_tags(categories_diff), unicode(cat1.pk)) + self.assertEqual(strip_tags(categories_diff), force_text(cat1.pk)) #check that it is really rollbacked self.assertFalse(Book.objects.filter(name='FooBook')) diff --git a/tests/core/tests/test.py b/tests/core/tests/test.py index d81392674..5c33bcb13 100644 --- a/tests/core/tests/test.py +++ b/tests/core/tests/test.py @@ -1,6 +1,8 @@ -from fields_tests import * -from widgets_tests import * -from resources_tests import * -from instance_loaders_tests import * -from admin_integration_tests import * -from base_formats_tests import * +from __future__ import unicode_literals + +from .fields_tests import * +from .widgets_tests import * +from .resources_tests import * +from .instance_loaders_tests import * +from .admin_integration_tests import * +from .base_formats_tests import * diff --git a/tests/core/tests/widgets_tests.py b/tests/core/tests/widgets_tests.py index 80e4d9d29..4e07a23c6 100644 --- a/tests/core/tests/widgets_tests.py +++ b/tests/core/tests/widgets_tests.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from decimal import Decimal from datetime import date @@ -5,7 +7,7 @@ from import_export import widgets -from ..models import ( +from core.models import ( Author, Category, ) diff --git a/tests/settings.py b/tests/settings.py index 8759fdb07..f5c6db35e 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import os INSTALLED_APPS = [ diff --git a/tests/urls.py b/tests/urls.py index ab9975b43..28039a035 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.conf.urls import patterns, include from django.contrib.staticfiles.urls import staticfiles_urlpatterns diff --git a/tox.ini b/tox.ini index bd2e614e6..f5ee2c6ca 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26-1.4, py27-1.4, py27-tablib-dev-1.4, py27-mysql-innodb-1.4, py27-1.5, py27-1.6 +envlist = py26-1.4, py27-1.4, py27-tablib-dev-1.4, py27-mysql-innodb-1.4, py27-1.5, py27-1.6, py33-1.6 [testenv] commands=python {toxinidir}/tests/manage.py test core @@ -37,3 +37,9 @@ deps = basepython = python2.7 deps = django==1.6 + +[testenv:py33-1.6] +basepython = python3.3 +deps = + django==1.6.1 + -egit+https://github.com/kennethreitz/tablib.git#egg=tablib