Python 3.3 support

This commit is contained in:
Bojan Mihelac 2014-01-26 12:12:39 +01:00
parent b307a8b2b6
commit 77c0ead3e7
28 changed files with 155 additions and 56 deletions

View File

@ -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
- if [[ $TRAVIS_PYTHON_VERSION != '3.3' && $DJANGO != "1.4.10" ]]; then python tests/manage.py test core --settings=settings; fi

View File

@ -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)

View File

@ -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)

View File

@ -1 +1 @@
__version__ = "0.1.7.dev0"
__version__ = "0.2.1.dev0"

View File

@ -21,6 +21,11 @@ from .resources import (
)
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 @@ class ImportMixin(object):
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 @@ class ImportMixin(object):
# 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)

View File

@ -1,3 +1,6 @@
from __future__ import unicode_literals
class ImportExportError(Exception):
"""A generic exception for all others to extend."""
pass

View File

@ -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

View File

@ -0,0 +1 @@
from __future__ import unicode_literals

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import warnings
import tablib
@ -15,6 +17,7 @@ except ImportError:
XLS_IMPORT = False
from django.utils.importlib import import_module
from django.utils import six
class Format(object):
@ -103,9 +106,18 @@ class TextFormat(TablibFormat):
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'

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django import forms
from django.utils.translation import ugettext_lazy as _

View File

@ -1,3 +1,6 @@
from __future__ import unicode_literals
class BaseInstanceLoader(object):
"""
Base abstract implementation of instance loader.

View File

@ -0,0 +1 @@
from __future__ import unicode_literals

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import functools
from copy import deepcopy
import sys
@ -8,6 +10,7 @@ from diff_match_patch import diff_match_patch
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 .results import Error, Result, RowResult
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 @@ class ResourceOptions(object):
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 @@ class DeclarativeMetaclass(type):
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 @@ class Resource(object):
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 @@ class Resource(object):
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 @@ class Resource(object):
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 @@ class ModelDeclarativeMetaclass(DeclarativeMetaclass):
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,

View File

@ -1,3 +1,6 @@
from __future__ import unicode_literals
class Error(object):
def __init__(self, error, traceback=None):

View File

@ -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 @@ class Widget(object):
"""
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):

View File

@ -10,6 +10,9 @@ CLASSIFIERS = [
'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 = [

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.contrib import admin
from import_export.admin import ImportExportMixin

View File

@ -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

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import os.path
from django.test.testcases import TestCase
@ -28,15 +30,16 @@ class ImportExportAdminIntegrationTest(TestCase):
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())

View File

@ -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 @@ class XLSTest(TestCase):
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)

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from datetime import date
from django.test import TestCase

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import tablib
from django.test import TestCase
@ -5,7 +7,7 @@ from django.test import TestCase
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 @@ class CachedInstanceLoaderTest(TestCase):
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):

View File

@ -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 widgets
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 @@ class ModelResourceTest(TestCase):
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 @@ class ModelResourceTest(TestCase):
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 @@ class ModelResourceTransactionTest(TransactionTestCase):
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'))

View File

@ -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 *

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from decimal import Decimal
from datetime import date
@ -5,7 +7,7 @@ from django.test import TestCase
from import_export import widgets
from ..models import (
from core.models import (
Author,
Category,
)

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import os
INSTALLED_APPS = [

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.conf.urls import patterns, include
from django.contrib.staticfiles.urls import staticfiles_urlpatterns

View File

@ -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