Commit 8c88be88 by Brian Mesick Committed by GitHub

Testing and Py3 compatibility changes (#5)

* Made Py3 compatible

* Added unit tests using pytest

* Travis CI, versioneye, codecov, and tox integrations.  

* Pointed to new `edx/pyfilesystem` dependency (needed to support Py3)

* Updated license to Apache 2.0
parent cc88f19e
[run]
branch = True
source = djpyfs
data_file = .coverage
omit = djpyfs/tests.py
[report]
exclude_lines =
pragma: no cover
raise NotImplementedError
...@@ -53,4 +53,12 @@ coverage.xml ...@@ -53,4 +53,12 @@ coverage.xml
docs/_build/ docs/_build/
# Emacs backups # Emacs backups
*~ *~
\ No newline at end of file
# PyCharm config
.idea/
# Paths generated in testing
django-pyfs/
example/db.sql
example/sample/static/
language: python
python:
- 2.7
- 3.5
env:
- TOXENV=django18
- TOXENV=django19
- TOXENV=django110
sudo: false
branches:
only:
- master
install:
- pip install -r requirements/test_requirements.txt
script:
- tox
after_success:
- bash <(curl -s https://codecov.io/bash)
from django.conf import settings
def pytest_configure():
settings.configure(
DEBUG=True,
USE_TZ=True,
DATABASES={
"default": {
"ENGINE": "django.db.backends.sqlite3",
}
},
ROOT_URLCONF="test_urls",
INSTALLED_APPS=[
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sites",
"django.contrib.sessions",
"djpyfs",
],
MIDDLEWARE_CLASSES=[
'django.contrib.sessions.middleware.SessionMiddleware',
'restrictedsessions.middleware.RestrictedSessionsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
],
SITE_ID=1,
)
...@@ -10,35 +10,27 @@ filesystem. ...@@ -10,35 +10,27 @@ filesystem.
task can garbage-collect those objects. task can garbage-collect those objects.
''' '''
from __future__ import absolute_import
import json
import os import os
import os.path import os.path
import types import types
from django.conf import settings from django.conf import settings
from fs.osfs import OSFS
from models import FSExpirations from .models import FSExpirations
if hasattr(settings, 'DJFS'): if hasattr(settings, 'DJFS'):
djfs_settings = settings.DJFS djfs_settings = settings.DJFS #pragma: no cover
else: else:
djfs_settings = {'type' : 'osfs', djfs_settings = {'type' : 'osfs',
'directory_root' : 'django-pyfs/static/django-pyfs', 'directory_root' : 'django-pyfs/static/django-pyfs',
'url_root' : '/static/django-pyfs'} 'url_root' : '/static/django-pyfs'}
if djfs_settings['type'] == 'osfs': s3conn = None
from fs.osfs import OSFS
elif djfs_settings['type'] == 's3fs':
from fs.s3fs import S3FS
from boto.s3.connection import S3Connection
from boto.s3.key import Key
key_id = djfs_settings.get('aws_access_key_id', None)
key_secret = djfs_settings.get('aws_secret_access_key', None)
s3conn = None
else:
raise AttributeError("Bad filesystem: "+str(djfs_settings['type']))
def get_filesystem(namespace): def get_filesystem(namespace):
''' Returns a pyfilesystem for static module storage. ''' Returns a pyfilesystem for static module storage.
...@@ -56,7 +48,7 @@ def get_filesystem(namespace): ...@@ -56,7 +48,7 @@ def get_filesystem(namespace):
def expire_objects(): def expire_objects():
''' Remove all obsolete objects from the file systems. Untested. ''' ''' Remove all obsolete objects from the file systems. Untested. '''
objects = sorted(FSExpirations.expired(), key=lambda x:x.module) objects = sorted(FSExpirations.expired(), key=lambda x: x.module)
fs = None fs = None
module = None module = None
for o in objects: for o in objects:
...@@ -98,12 +90,20 @@ def get_osfs(namespace): ...@@ -98,12 +90,20 @@ def get_osfs(namespace):
if not os.path.exists(full_path): if not os.path.exists(full_path):
os.makedirs(full_path) os.makedirs(full_path)
osfs = OSFS(full_path) osfs = OSFS(full_path)
osfs = patch_fs(osfs, namespace, lambda self, filename, timeout=0:os.path.join(djfs_settings['url_root'], namespace, filename)) osfs = patch_fs(osfs, namespace, lambda self, filename, timeout=0: os.path.join(djfs_settings['url_root'], namespace, filename))
return osfs return osfs
def get_s3fs(namespace): def get_s3fs(namespace):
''' Helper method to get_filesystem for a file system on S3 ''' ''' Helper method to get_filesystem for a file system on S3 '''
global key_id, key_secret # Our test suite does not presume Amazon S3, and we would prefer not to have a global import so that we can run
# tests without requiring boto. These will become global when and if we include S3/boto in our test suite.
from fs.s3fs import S3FS
from boto.s3.connection import S3Connection
key_id = djfs_settings.get('aws_access_key_id', None)
key_secret = djfs_settings.get('aws_secret_access_key', None)
s3conn = None
fullpath = namespace fullpath = namespace
if 'prefix' in djfs_settings: if 'prefix' in djfs_settings:
fullpath = os.path.join(djfs_settings['prefix'], fullpath) fullpath = os.path.join(djfs_settings['prefix'], fullpath)
...@@ -121,4 +121,3 @@ def get_s3fs(namespace): ...@@ -121,4 +121,3 @@ def get_s3fs(namespace):
s3fs = patch_fs(s3fs, namespace, get_s3_url) s3fs = patch_fs(s3fs, namespace, get_s3_url)
return s3fs return s3fs
import django import os
from django.db import models
import datetime
from django.db import models
from django.utils import timezone from django.utils import timezone
## Create your models here.
#class StudentBookAccesses(models.Model):
# username = models.CharField(max_length=500, unique=True) # TODO: Should not have max_length
# count = models.IntegerField()
class FSExpirations(models.Model): class FSExpirations(models.Model):
''' The modules have access to a pyfilesystem object where they '''
Model to handle expiring temporary files.
The modules have access to a pyfilesystem object where they
can store big data, images, etc. In most cases, we would like can store big data, images, etc. In most cases, we would like
those objects to expire (e.g. if a view generates a .PNG analytic those objects to expire (e.g. if a view generates a .PNG analytic
to show to a user). This model keeps track of files stored, as to show to a user). This model keeps track of files stored, as
well as the expirations of those models. well as the expirations of those models.
''' '''
module = models.CharField(max_length=382) # Defines the namespace
filename = models.CharField(max_length=382) # Filename within the namespace
expires = models.BooleanField() # Does it expire?
expiration = models.DateTimeField(db_index=True)
@classmethod @classmethod
def create_expiration(cls, module, filename, seconds, days=0, expires = True): def create_expiration(cls, module, filename, seconds, days=0, expires = True):
''' May be used instead of the constructor to create a new expiration. '''
May be used instead of the constructor to create a new expiration.
Automatically applies timedelta and saves to DB. Automatically applies timedelta and saves to DB.
''' '''
expiration_time = timezone.now() + timezone.timedelta(days, seconds) expiration_time = timezone.now() + timezone.timedelta(days, seconds)
# If object exists, update it # If object exists, update it
objects = cls.objects.filter(module = module, filename = filename) objects = cls.objects.filter(module=module, filename=filename)
if objects: if objects:
exp = objects[0] exp = objects[0]
exp.expires = expires exp.expires = expires
...@@ -41,30 +44,26 @@ class FSExpirations(models.Model): ...@@ -41,30 +44,26 @@ class FSExpirations(models.Model):
f.expiration = expiration_time f.expiration = expiration_time
f.save() f.save()
module = models.CharField(max_length=382) # Defines the namespace
filename = models.CharField(max_length=382) # Filename within the namespace
expires = models.BooleanField() # Does it expire?
expiration = models.DateTimeField(db_index = True)
@classmethod @classmethod
def expired(cls): def expired(cls):
''' Returns a list of expired objects ''' '''
Returns a list of expired objects
'''
expiration_lte = timezone.now() expiration_lte = timezone.now()
return cls.objects.filter(expires=True, expiration__lte = expiration_lte) return cls.objects.filter(expires=True, expiration__lte = expiration_lte)
class Meta: class Meta(object):
app_label = 'djpyfs' app_label = 'djpyfs'
unique_together = (("module","filename")) unique_together = (("module", "filename"),)
# We'd like to create an index first on expiration than on expires (so we can # We'd like to create an index first on expiration than on expires (so we can
# search for objects where expires=True and expiration is before now). # search for objects where expires=True and expiration is before now).
index_together = [ index_together = [
["expiration", "expires"], ["expiration", "expires"],
] ]
def __str__(self): def __str__(self):
if self.expires: if self.expires:
return self.module+'/'+self.filename+" Expires "+str(self.expiration) return "{} Expires {}".format(os.path.join(self.module, self.filename), str(self.expiration))
else: else:
return self.module+'/'+self.filename+" Permanent ("+str(self.expiration)+")" return "{} Permanent ({})".format(os.path.join(self.module, self.filename), str(self.expiration))
This diff is collapsed. Click to expand it.
from future.builtins import map
import png import png
from django.http import HttpResponse from django.http import HttpResponse
...@@ -10,14 +11,17 @@ arrow = ["11011", ...@@ -10,14 +11,17 @@ arrow = ["11011",
"11011", "11011",
"11011", "11011",
"10001"] "10001"]
arrow = [map(int, x) for x in arrow] arrow = [list(map(int, x)) for x in arrow]
def index(request):
def index(_):
fs = djpyfs.get_filesystem("sample") fs = djpyfs.get_filesystem("sample")
f = fs.open("uparrow.png", "wb") f = fs.open("uparrow.png", "wb")
png.Writer(len(arrow[0]), len(arrow), greyscale = True, bitdepth = 1).write(f, arrow) png.Writer(len(arrow[0]), len(arrow), greyscale=True, bitdepth=1).write(f, arrow)
f.close() f.close()
url = fs.get_url("uparrow.png") url = fs.get_url("uparrow.png")
return HttpResponse("<html><body>Hello, world. You're at the polls index. <img src=\"{source}\"> </body></html>".format(source=url)) return HttpResponse(
"<html><body>Hello, world. You're at the test index. <img src=\"{source}\"> </body></html>".format(source=url)
)
[pytest]
python_files = djpyfs/tests.py
addopts = --cov djpyfs --cov-report term-missing
# This requirement breaks both pip-tools and versioneye. The requirements are unchanged from master. It is possible to
# temporarily change this to fs for updating pinned versions using pip-tools, then change it back after, but you will
# have to hand edit the resulting requirements.txt to replace this url since it contains changes necessary for Python 3.
git+https://github.com/edx/pyfilesystem.git@bmedx/s3fs-py3-support#egg=fs==0.5.5a1
-r github.txt
six==1.10.0
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --output-file requirements/requirements.txt requirements/requirements.in
#
appdirs==1.4.0 # via fs
enum34==1.1.6 # via fs
# This line is NOT autogenerated, see requirements/requirements.in for details.
-r github.txt
pytz==2016.10 # via fs
scandir==1.4 # via fs
six==1.10.0
# Additional requirements for unit tests, you must also run the main requirements.txt.
boto==2.45.0
codecov==2.0.5
moto==0.4.30
pytest==3.0.5
pytest-django==3.1.2
pytest-cov==2.4.0
tox==2.5.0
# For the sample app
pypng==0.0.18
# Uncomment if you want to run the tests standalone. Tox will test against multiple versions by default.
# Django>=1.8
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --output-file requirements/test_requirements.txt requirements/test_requirements.in
#
argparse==1.4.0 # via codecov
boto==2.45.0
codecov==2.0.5
coverage==4.3.1 # via codecov, pytest-cov
httpretty==0.8.10 # via moto
jinja2==2.8.1 # via moto
markupsafe==0.23 # via jinja2
moto==0.4.30
pluggy==0.4.0 # via tox
py==1.4.32 # via pytest, tox
pypng==0.0.18
pytest-cov==2.4.0
pytest-django==3.1.2
pytest==3.0.5
python-dateutil==2.6.0 # via moto
pytz==2016.10 # via moto
requests==2.12.4 # via codecov, moto
six==1.10.0 # via moto, python-dateutil
tox==2.5.0
virtualenv==15.1.0 # via tox
werkzeug==0.11.15 # via moto
xmltodict==0.10.2 # via moto
...@@ -17,13 +17,22 @@ setup( ...@@ -17,13 +17,22 @@ setup(
author='Piotr Mitros', author='Piotr Mitros',
author_email='pmitros@edx.org', author_email='pmitros@edx.org',
packages=['djpyfs'], packages=['djpyfs'],
license = "AGPLv3", license="Apache 2.0",
url = "https://github.com/edx/django-pyfs", url="https://github.com/edx/django-pyfs",
long_description = ld, long_description=ld,
classifiers = [ classifiers=[
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
"Framework :: Django",
"Framework :: Django :: 1.8",
"Framework :: Django :: 1.9",
"Framework :: Django :: 1.10",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", "License :: OSI Approved :: Apache Software License",
], ],
install_requires=['fs'], install_requires=['fs', 'six'],
) )
from django.conf.urls import patterns, url
from django.http import HttpResponse
urlpatterns = patterns(
'',
url(
regex=r'test_view/',
view=lambda r: HttpResponse(content="For unittests", status=200),
name='test_view'
),
)
\ No newline at end of file
# Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.
[tox]
envlist = {py27,py35}-{django18,django19,django110}
[testenv]
passenv = CI TRAVIS TRAVIS_*
commands = pytest
deps =
-r{toxinidir}/requirements/requirements.txt
-r{toxinidir}/requirements/test_requirements.txt
django18: django==1.8
django19: django==1.9
django110: django==1.10
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment