django-pyscss - use PySCSS in Django more easily
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.egg-info/
|
||||
.sass-cache/
|
||||
tmp/
|
||||
22
LICENSE
Normal file
22
LICENSE
Normal 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
3
MANIFEST.in
Normal file
@@ -0,0 +1,3 @@
|
||||
include README.rst
|
||||
include LICENSE
|
||||
recursive-include extras *
|
||||
24
README.rst
Normal file
24
README.rst
Normal 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
|
||||
0
django_pyscss/__init__.py
Normal file
0
django_pyscss/__init__.py
Normal file
94
django_pyscss/scss.py
Normal file
94
django_pyscss/scss.py
Normal 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
10
manage.py
Executable 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
43
setup.py
Normal 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
0
testproject/__init__.py
Normal file
15
testproject/runtests.py
Normal file
15
testproject/runtests.py
Normal 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))
|
||||
0
testproject/testapp1/__init__.py
Normal file
0
testproject/testapp1/__init__.py
Normal file
3
testproject/testapp1/admin.py
Normal file
3
testproject/testapp1/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
3
testproject/testapp1/models.py
Normal file
3
testproject/testapp1/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
3
testproject/testapp1/static/css/app1.scss
Normal file
3
testproject/testapp1/static/css/app1.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.app1 {
|
||||
color: #ff9900;
|
||||
}
|
||||
3
testproject/testapp1/tests.py
Normal file
3
testproject/testapp1/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
testproject/testapp1/views.py
Normal file
3
testproject/testapp1/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
0
testproject/testapp2/__init__.py
Normal file
0
testproject/testapp2/__init__.py
Normal file
3
testproject/testapp2/admin.py
Normal file
3
testproject/testapp2/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
3
testproject/testapp2/models.py
Normal file
3
testproject/testapp2/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
2
testproject/testapp2/static/css/app2.scss
Normal file
2
testproject/testapp2/static/css/app2.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
@import "css/foo.scss";
|
||||
@import "css/app1.scss";
|
||||
3
testproject/testapp2/tests.py
Normal file
3
testproject/testapp2/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
testproject/testapp2/views.py
Normal file
3
testproject/testapp2/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
0
testproject/testproject/__init__.py
Normal file
0
testproject/testproject/__init__.py
Normal file
90
testproject/testproject/settings.py
Normal file
90
testproject/testproject/settings.py
Normal 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'),
|
||||
)
|
||||
3
testproject/testproject/static/css/foo.scss
Normal file
3
testproject/testproject/static/css/foo.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.foo {
|
||||
color: #ff0000;
|
||||
}
|
||||
12
testproject/testproject/urls.py
Normal file
12
testproject/testproject/urls.py
Normal 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)),
|
||||
)
|
||||
14
testproject/testproject/wsgi.py
Normal file
14
testproject/testproject/wsgi.py
Normal 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
0
tests/__init__.py
Normal file
64
tests/test_import.py
Normal file
64
tests/test_import.py
Normal 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
|
||||
Reference in New Issue
Block a user