Python3 and Django 1.11 upgrade (#40)

* starting py3 upgrade

* b64 not base64

* more progress, some more future things, and encoding

* tox

* update circle.yml

* that version doesnt exist yet, oops

* install?

* sigh

* Docker me?

* update readme, requirements

* fix for python3

* python35

* morgan remediations

* does this work?

* how about this?

* one more time

* no blank lines?

* revert a bit

* add future imports
This commit is contained in:
Tyrel Souza 2018-03-01 15:32:02 -05:00 committed by GitHub
parent fc974242ba
commit 4b7738c0f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 88 additions and 47 deletions

View File

@ -7,6 +7,18 @@ Easy to use for testing remote storages when you're in a transition stage betwee
Intended to be used in tests, never in production. Intended to be used in tests, never in production.
## TESTING
I use Pyenv and Tox to support Python2.7.13 and Python3.6.3
```
pip install -r requirements-dev.txt
pyenv install 2.7.13 3.5.4 3.6.3
tox
```
Or you can run individually with your shell python using `python setup.py test`
## INSTALLATION ## INSTALLATION
``` ```
@ -42,7 +54,7 @@ class SomeClass(models.Model):
## TODO ## TODO
- Test that this works on a fake model, not just the storage file. - Test that this works on a fake model, not just the storage file.
- Different django and different python versions. - Different django versions.
## Signing Key ## Signing Key
You can find my signing key at [TyrelSouza.com](https://tyrelsouza.com/koken/?/pages/pgp-keys/) You can find my signing key at [TyrelSouza.com](https://tyrelsouza.com/koken/?/pages/pgp-keys/)
@ -51,8 +63,9 @@ I will sign everything with 0x769A1BC78A2DDEE2
## CHANGELOG ## CHANGELOG
- 2018-02-01 [Tyrel Souza] Bump versions to django 1.11 and testing with Python3
- 2017-05-10 [Pamela McA'Nulty] Have save overwrite existing files - 2017-05-10 [Pamela McA'Nulty] Have save overwrite existing files
- 2017-02-06 [Tyrel Souza] Set primary key to `id` not `name`, this involves a lot of migrations, so I've kept them in multiple files - 2017-02-06 [Tyrel Souza] Set primary key to `id` not `name`, this involves a lot of migrations, so I've kept them in multiple files
- 2017-01-27 [Tyrel Souza] Get rid of filehash - 2017-01-27 [Tyrel Souza] Get rid of filehash
- 2017-01-26 [Tyrel Souza] Check filehash and filename, not just hash when checking if it needs to be saved. - 2017-01-26 [Tyrel Souza] Check filehash and filename, not just hash when checking if it needs to be saved.
- 2017-01-25 [Tyrel Souza] Keeping Filename on upload. - 2017-01-25 [Tyrel Souza] Keeping Filename on upload.

View File

@ -1,3 +1,14 @@
version: 2
jobs:
build:
working_directory: ~/django-dbfilestorage
docker:
- image: themattrix/tox
steps:
- checkout
- run: tox
test: test:
post: post:
- bash <(curl -s https://codecov.io/bash) - bash <(curl -s https://codecov.io/bash)

View File

@ -1,3 +1,5 @@
from __future__ import absolute_import, print_function, unicode_literals
from django.contrib import admin from django.contrib import admin
from .models import DBFile from .models import DBFile

View File

@ -1,6 +1,10 @@
from __future__ import absolute_import, print_function, unicode_literals
from django.db import models from django.db import models
from future.utils import python_2_unicode_compatible
@python_2_unicode_compatible
class DBFile(models.Model): class DBFile(models.Model):
""" Model to store and access uploaded files """ """ Model to store and access uploaded files """
@ -11,7 +15,7 @@ class DBFile(models.Model):
b64 = models.TextField() b64 = models.TextField()
mtime = models.DateTimeField(auto_now=True) mtime = models.DateTimeField(auto_now=True)
def __unicode__(self): def __str__(self):
return u"{name} <{content_type}>".format( return u"{name} <{content_type}>".format(
name=self.name, content_type=self.content_type) name=self.name, content_type=self.content_type)

View File

@ -1,3 +1,6 @@
from __future__ import absolute_import, print_function, unicode_literals
import base64
import mimetypes import mimetypes
import logging import logging
import os import os
@ -6,7 +9,7 @@ from django.db.transaction import atomic
from django.db.models import Q from django.db.models import Q
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.storage import Storage from django.core.files.storage import Storage
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse_lazy
from .models import DBFile from .models import DBFile
@ -29,7 +32,7 @@ class DBFileStorage(Storage):
def _open(self, name, mode='rb'): def _open(self, name, mode='rb'):
the_file = _get_object(name) the_file = _get_object(name)
return ContentFile(the_file.b64.decode('base64')) return ContentFile(base64.b64decode(the_file.b64))
@atomic @atomic
def _save(self, name, content, max_length=None): def _save(self, name, content, max_length=None):
@ -43,8 +46,13 @@ class DBFileStorage(Storage):
Then it checks if the file exists and if it doesn't, it will create Then it checks if the file exists and if it doesn't, it will create
the entry in the database. the entry in the database.
:param name: file name to save
:param content: Content object, where content.file is bytes
:return str: the name(md5) to look up the file by. :return str: the name(md5) to look up the file by.
""" """
# TODO: Make this conditional better
if hasattr(content.file, "read"): if hasattr(content.file, "read"):
read_data = content.file.read() read_data = content.file.read()
if not read_data: if not read_data:
@ -52,8 +60,9 @@ class DBFileStorage(Storage):
content.file.seek(0) content.file.seek(0)
read_data = content.file.read() read_data = content.file.read()
else: else:
read_data = content.file.encode('utf8') read_data = content.file
b64 = read_data.encode('base64')
b64 = base64.b64encode(read_data)
# USE mimetypes.guess_type as an attempt at getting the content type. # USE mimetypes.guess_type as an attempt at getting the content type.
ct = mimetypes.guess_type(name)[0] ct = mimetypes.guess_type(name)[0]
@ -91,8 +100,8 @@ class DBFileStorage(Storage):
def url(self, name): def url(self, name):
dbf = _get_object(name) dbf = _get_object(name)
if dbf: if dbf:
return reverse('dbstorage_file', args=(dbf.name,)) return reverse_lazy('dbstorage_file', args=(dbf.name,))
return reverse('dbstorage_file', args=(name,)) return reverse_lazy('dbstorage_file', args=(name,))
def modified_time(self, name): def modified_time(self, name):
dbf = _get_object(name) dbf = _get_object(name)

View File

@ -1,5 +1,7 @@
from __future__ import absolute_import, print_function, unicode_literals
from django.conf.urls import url from django.conf.urls import url
import views from dbfilestorage import views
urlpatterns = [ urlpatterns = [
url(r'^(?P<name>.*)$', views.show_file, name="dbstorage_file"), url(r'^(?P<name>.*)$', views.show_file, name="dbstorage_file"),

View File

@ -1,3 +1,7 @@
from __future__ import absolute_import, print_function, unicode_literals
import base64
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
from django.db.models import Q from django.db.models import Q
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
@ -15,7 +19,7 @@ def show_file(request, name):
""" """
dbf = get_object_or_404(DBFile, name=name) dbf = get_object_or_404(DBFile, name=name)
response = HttpResponse( response = HttpResponse(
dbf.b64.decode('base64'), base64.b64decode(dbf.b64),
content_type=dbf.content_type) content_type=dbf.content_type)
response['Content-Disposition'] = 'attachment; filename="{}"'.format( response['Content-Disposition'] = 'attachment; filename="{}"'.format(
dbf.name) dbf.name)

4
requirements-dev.txt Normal file
View File

@ -0,0 +1,4 @@
Sphinx==1.5
coverage==4.2
future
tox

View File

@ -1,3 +1 @@
Django==1.10.4 Django==1.11.10
Sphinx==1.5
coverage==4.2

View File

@ -21,10 +21,11 @@ class CleanCommand(Command):
os.system('rm -vrf ./build ./dist ./*.pyc ./*.tgz ./*.egg-info') os.system('rm -vrf ./build ./dist ./*.pyc ./*.tgz ./*.egg-info')
os.system('rm -vrf htmlcov .coverage') os.system('rm -vrf htmlcov .coverage')
os.system('rm -vrf .DS_Store') os.system('rm -vrf .DS_Store')
os.system('rm -vrf .tox')
setup( setup(
name="django-dbfilestorage", name="django-dbfilestorage",
version="0.9.2", version="0.9.3",
description="Database backed file storage for testing.", description="Database backed file storage for testing.",
long_description="Database backed file storage for testing. Stores files as base64 encoded textfields.", long_description="Database backed file storage for testing. Stores files as base64 encoded textfields.",
author="Tyrel Souza", author="Tyrel Souza",
@ -38,11 +39,12 @@ setup(
], ],
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=[
"Django>=1.8.0", "Django==1.11.10",
], ],
tests_require=[ tests_require=[
"nose", "nose",
"coverage", "coverage",
"future",
], ],
zip_safe=False, zip_safe=False,
test_suite="tests.runtests.start", test_suite="tests.runtests.start",

View File

@ -1,4 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
import base64
import os import os
from dbfilestorage.models import DBFile from dbfilestorage.models import DBFile
@ -11,6 +13,7 @@ from django.test import TestCase, Client
from django.test.utils import override_settings from django.test.utils import override_settings
PROJECT_ROOT = os.path.dirname(os.path.realpath(__file__)) PROJECT_ROOT = os.path.dirname(os.path.realpath(__file__))
DEFAULT_FILE_STORAGE = "dbfilestorage.storage.DBFileStorage" DEFAULT_FILE_STORAGE = "dbfilestorage.storage.DBFileStorage"
@ -36,10 +39,10 @@ class DBFileTest(TestCase):
def test_content_file(self): def test_content_file(self):
""" Test that this code works with ContentFile as well """ """ Test that this code works with ContentFile as well """
content_file = ContentFile(u"ƊBStørage") content_file = ContentFile(b"\\u018aBSt\\xf8rage")
default_storage.save("unicode", content_file) default_storage.save("unicode", content_file)
unicode_file = DBFile.objects.get(name="unicode") unicode_file = DBFile.objects.get(name="unicode")
self.assertEqual(unicode(unicode_file), self.assertEqual(str(unicode_file),
"unicode <application/octet-stream>") "unicode <application/octet-stream>")
def test_no_duplicate_upload(self): def test_no_duplicate_upload(self):
@ -52,7 +55,7 @@ class DBFileTest(TestCase):
""" Test that the DB entry matches what is expected from the file """ """ Test that the DB entry matches what is expected from the file """
with open(self.filepath, 'rb') as f: with open(self.filepath, 'rb') as f:
dbf = DBFile.objects.get(name=self.filepath) dbf = DBFile.objects.get(name=self.filepath)
self.assertEqual(dbf.b64.decode("base64"), f.read()) self.assertEqual(base64.b64decode(dbf.b64), f.read())
self.assertEqual(dbf.content_type, 'image/jpeg') self.assertEqual(dbf.content_type, 'image/jpeg')
def test_open(self): def test_open(self):
@ -62,7 +65,7 @@ class DBFileTest(TestCase):
self.assertEqual(dbf.read(), f.read()) self.assertEqual(dbf.read(), f.read())
def test_exists(self): def test_exists(self):
""" Test that the storage mechanism can check existance """ """ Test that the storage mechanism can check existence """
self.assertTrue(default_storage.exists(self.filepath)) self.assertTrue(default_storage.exists(self.filepath))
def test_delete(self): def test_delete(self):
@ -81,8 +84,8 @@ class DBFileTest(TestCase):
self.assertGreater(size, 0) self.assertGreater(size, 0)
def test_raw_save(self): def test_raw_save(self):
CONTENT_DATA_1 = u"Here's some stuff! ƊBStørage - ONE" CONTENT_DATA_1 = "Here's some stuff! ƊBStørage - ONE"
CONTENT_DATA_2 = u"Here's some stuff! ƊBStørage - TWO" CONTENT_DATA_2 = "Here's some stuff! ƊBStørage - TWO"
FILE_NAME = "saveable.txt" FILE_NAME = "saveable.txt"
self.assertFalse(DBFile.objects.filter(name=FILE_NAME).exists()) self.assertFalse(DBFile.objects.filter(name=FILE_NAME).exists())
@ -142,9 +145,9 @@ class DBFileTest(TestCase):
def test_listdir(self): def test_listdir(self):
""" Make sure listdir works, and only returns things under 'dirname' """ """ Make sure listdir works, and only returns things under 'dirname' """
names = [ names = [
u'dirname/kris.jpg', 'dirname/kris.jpg',
u'dirname/kris2.jpg', 'dirname/kris2.jpg',
u'dirname/kris3.jpg'] 'dirname/kris3.jpg']
for name in names: for name in names:
self._upload(name=name) self._upload(name=name)

View File

@ -1,18 +1,5 @@
"""tests URL Configuration from __future__ import absolute_import, print_function, unicode_literals
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.10/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import url, include from django.conf.urls import url, include
from django.contrib import admin from django.contrib import admin

View File

@ -1,11 +1,4 @@
""" from __future__ import absolute_import, print_function, unicode_literals
WSGI config for tests project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""
import os import os

9
tox.ini Normal file
View File

@ -0,0 +1,9 @@
[tox]
envlist = py27,py35,py36
[testenv]
commands=pip install -e .
pip install -r requirements.txt
python setup.py test
install_command=pip install --process-dependency-links --allow-external --allow-unverified {opts} {packages}