django-pyscss - use PySCSS in Django more easily

This commit is contained in:
Rocky Meza
2014-02-01 18:22:17 -07:00
commit 2a53a7abd1
29 changed files with 426 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*.egg-info/
.sass-cache/
tmp/

22
LICENSE Normal file
View File

@@ -0,0 +1,22 @@
Copyright (c) 2013, Fusionbox, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

3
MANIFEST.in Normal file
View File

@@ -0,0 +1,3 @@
include README.rst
include LICENSE
recursive-include extras *

24
README.rst Normal file
View File

@@ -0,0 +1,24 @@
django-pyscss
-------------
A collection of tools for making it easier to use PyScss within Django.
.. class:: django_pyscss.scss.DjangoScss
A subclass of :class:`scss.Scss` that uses the Django staticfiles storage
and finders instead of the filesystem. This obseletes the load_paths
option that was present previously by searching instead in your staticfiles
directories.
In DEBUG mode, DjangoScss will search using all of the finders to find the
file. If you are not in DEBUG mode, it assumes you have run collectstatic
and will only use staticfiles_storage to find the file.
Running the tests
=================
You first have to run `./manage.py collectstatic` before you can run the tests
for the first time. After that, you can just run::
$ python setup.py test

View File

94
django_pyscss/scss.py Normal file
View File

@@ -0,0 +1,94 @@
from __future__ import absolute_import, unicode_literals
import fnmatch
from django.contrib.staticfiles import finders
from django.contrib.staticfiles.storage import staticfiles_storage
from django.conf import settings
from scss import (
Scss, dequote, log, spawn_rule,
OPTIONS, PROPERTIES,
)
def find_all_files(glob):
"""
Finds all files in the django finders for a given glob,
returns the file path, if available, and the django storage object.
storage objects must implement the File storage API:
https://docs.djangoproject.com/en/dev/ref/files/storage/
"""
for finder in finders.get_finders():
for path, storage in finder.list([]):
if fnmatch.fnmatchcase(path, glob):
yield path, storage
def find_one_file(path):
for file in find_all_files(path):
return file
class DjangoScss(Scss):
"""
A subclass of the Scss compiler that uses the storages API for accessing
files.
"""
def get_file_from_storage(self, filename):
try:
filename = staticfiles_storage.path(filename)
except NotImplementedError:
# remote storages don't implement path
pass
finally:
with staticfiles_storage.open(filename) as f:
return f.read()
def get_file_from_finders(self, filename):
path, storage = find_one_file(filename)
with storage.open(path) as f:
return f.read()
def get_file_contents(self, filename):
# TODO: the switch probably shouldn't be on DEBUG
if settings.DEBUG:
return self.get_file_from_finders(filename)
else:
return self.get_file_from_storage(filename)
def _do_import(self, rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name):
"""
Implements @import using the django storages API.
"""
# Protect against going to prohibited places...
if '..' in name or '://' in name or 'url(' in name:
rule[PROPERTIES].append((c_lineno, c_property, None))
return
full_filename = None
i_codestr = None
names = name.split(',')
for filename in names:
filename = dequote(name.strip())
if '@import ' + filename in rule[OPTIONS]:
# If already imported in this scope, skip
continue
try:
i_codestr = self.scss_files[filename]
except KeyError:
i_codestr = self.get_file_contents(filename)
if i_codestr is None:
i_codestr = self._do_magic_import(rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name)
if i_codestr is None:
log.warn("I couldn't find this file (%s)." % filename)
i_codestr = self.scss_files[name] = i_codestr and self.load_string(i_codestr, full_filename)
if name not in self.scss_files:
self._scss_files_order.append(name)
if i_codestr is not None:
_rule = spawn_rule(rule, codestr=i_codestr, path=full_filename, lineno=c_lineno)
self.manage_children(_rule, p_selectors, p_parents, p_children, scope, media)
rule[OPTIONS]['@import ' + name] = True

10
manage.py Executable file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.testproject.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)

43
setup.py Normal file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python
from setuptools import setup, find_packages
import subprocess
import os
__doc__ = """
Makes it easier to use PySCSS in Django.
"""
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
install_requires = [
'Django>=1.3',
'PyScss>=1.1.5',
]
version = (0, 0, 1, 'final')
def get_version():
number = '.'.join(map(str, version[:3]))
stage = version[3]
if stage == 'final':
return number
elif stage == 'alpha':
process = subprocess.Popen('git rev-parse HEAD'.split(), stdout=subprocess.PIPE)
stdout, stderr = process.communicate()
return number + '-' + stdout.strip()[:8]
setup(
name='django-pyscss',
version=get_version(),
description=__doc__,
long_description=read('README.rst'),
packages=[package for package in find_packages() if package.startswith('django_pyscss')],
install_requires=install_requires,
zip_safe=False,
include_package_data=True,
test_suite='testproject.runtests.runtests',
)

0
testproject/__init__.py Normal file
View File

15
testproject/runtests.py Normal file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env python
import os
import sys
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.testproject.settings")
from django.test.utils import get_runner
from django.conf import settings
def runtests():
# Stolen from django/core/management/commands/test.py
TestRunner = get_runner(settings)
test_runner = TestRunner(verbosity=1, interactive=True)
failures = test_runner.run_tests(['tests'])
sys.exit(bool(failures))

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@@ -0,0 +1,3 @@
.app1 {
color: #ff9900;
}

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@@ -0,0 +1,2 @@
@import "css/foo.scss";
@import "css/app1.scss";

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

View File

@@ -0,0 +1,90 @@
"""
Django settings for testproject project.
For more information on this file, see
https://docs.djangoproject.com/en/1.6/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.6/ref/settings/
"""
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'testproject'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
TEMPLATE_DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'testproject.testapp1',
'testproject.testapp2',
)
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
ROOT_URLCONF = 'testproject.urls'
WSGI_APPLICATION = 'testproject.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.6/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Internationalization
# https://docs.djangoproject.com/en/1.6/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.6/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, '..', 'tmp', 'static')
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'testproject', 'static'),
)

View File

@@ -0,0 +1,3 @@
.foo {
color: #ff0000;
}

View File

@@ -0,0 +1,12 @@
from django.conf.urls import patterns, include, url
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
# Examples:
# url(r'^$', 'testproject.views.home', name='home'),
# url(r'^blog/', include('blog.urls')),
url(r'^admin/', include(admin.site.urls)),
)

View File

@@ -0,0 +1,14 @@
"""
WSGI config for testproject 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.6/howto/deployment/wsgi/
"""
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings")
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

0
tests/__init__.py Normal file
View File

64
tests/test_import.py Normal file
View File

@@ -0,0 +1,64 @@
import os
from django.test import TestCase
from django.test.utils import override_settings
from django.conf import settings
from django_pyscss.scss import DjangoScss
compiler = DjangoScss(scss_opts={
# No compress so that I can compare more easily
'compress': 0,
})
def compile_string(string):
return compiler.compile(scss_string=string)
IMPORT_FOO = """
@import "css/foo.scss";
"""
with open(os.path.join(settings.BASE_DIR, 'testproject', 'static', 'css', 'foo.scss')) as f:
FOO_CONTENTS = f.read()
IMPORT_APP1 = """
@import "css/app1.scss";
"""
with open(os.path.join(settings.BASE_DIR, 'testapp1', 'static', 'css', 'app1.scss')) as f:
APP1_CONTENTS = f.read()
IMPORT_APP2 = """
@import "css/app2.scss";
"""
APP2_CONTENTS = FOO_CONTENTS + APP1_CONTENTS
class ImportTestMixin(object):
def test_import_from_staticfiles_dirs(self):
actual = compile_string(IMPORT_FOO)
self.assertEqual(actual.strip(), FOO_CONTENTS.strip())
def test_import_from_app(self):
actual = compile_string(IMPORT_APP1)
self.assertEqual(actual.strip(), APP1_CONTENTS.strip())
def test_imports_within_file(self):
actual = compile_string(IMPORT_APP2)
self.assertEqual(actual.strip(), APP2_CONTENTS.strip())
@override_settings(DEBUG=True)
class FindersImportTest(ImportTestMixin, TestCase):
pass
@override_settings(DEBUG=False)
class StorageImportTest(ImportTestMixin, TestCase):
pass