Python 3.3 support
This commit is contained in:
parent
b307a8b2b6
commit
77c0ead3e7
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.7.dev0"
|
||||
__version__ = "0.2.1.dev0"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
class ImportExportError(Exception):
|
||||
"""A generic exception for all others to extend."""
|
||||
pass
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
from __future__ import unicode_literals
|
|
@ -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'
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
class BaseInstanceLoader(object):
|
||||
"""
|
||||
Base abstract implementation of instance loader.
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
from __future__ import unicode_literals
|
|
@ -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,
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
class Error(object):
|
||||
|
||||
def __init__(self, error, traceback=None):
|
||||
|
|
|
@ -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):
|
||||
|
|
3
setup.py
3
setup.py
|
@ -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 = [
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from import_export.admin import ImportExportMixin
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import date
|
||||
|
||||
from django.test import TestCase
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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 *
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
INSTALLED_APPS = [
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import patterns, include
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
|
||||
|
|
8
tox.ini
8
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
|
||||
|
|
Loading…
Reference in New Issue