summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRocky Meza <rocky@fusionbox.com>2014-02-01 18:22:17 -0700
committerRocky Meza <rocky@fusionbox.com>2014-02-01 19:05:04 -0700
commit2a53a7abd1ddba02c44b81a24305b03532b45fee (patch)
tree6b009211aa1eb8355464dcf0e765b076207fa778
downloaddjango-pyscss-2a53a7abd1ddba02c44b81a24305b03532b45fee.tar.gz
django-pyscss - use PySCSS in Django more easily
-rw-r--r--.gitignore3
-rw-r--r--LICENSE22
-rw-r--r--MANIFEST.in3
-rw-r--r--README.rst24
-rw-r--r--django_pyscss/__init__.py0
-rw-r--r--django_pyscss/scss.py94
-rwxr-xr-xmanage.py10
-rw-r--r--setup.py43
-rw-r--r--testproject/__init__.py0
-rw-r--r--testproject/runtests.py15
-rw-r--r--testproject/testapp1/__init__.py0
-rw-r--r--testproject/testapp1/admin.py3
-rw-r--r--testproject/testapp1/models.py3
-rw-r--r--testproject/testapp1/static/css/app1.scss3
-rw-r--r--testproject/testapp1/tests.py3
-rw-r--r--testproject/testapp1/views.py3
-rw-r--r--testproject/testapp2/__init__.py0
-rw-r--r--testproject/testapp2/admin.py3
-rw-r--r--testproject/testapp2/models.py3
-rw-r--r--testproject/testapp2/static/css/app2.scss2
-rw-r--r--testproject/testapp2/tests.py3
-rw-r--r--testproject/testapp2/views.py3
-rw-r--r--testproject/testproject/__init__.py0
-rw-r--r--testproject/testproject/settings.py90
-rw-r--r--testproject/testproject/static/css/foo.scss3
-rw-r--r--testproject/testproject/urls.py12
-rw-r--r--testproject/testproject/wsgi.py14
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/test_import.py64
29 files changed, 426 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fc09f94
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*.egg-info/
+.sass-cache/
+tmp/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..153031a
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..587bec0
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,3 @@
+include README.rst
+include LICENSE
+recursive-include extras *
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..cb82ae5
--- /dev/null
+++ b/README.rst
@@ -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
diff --git a/django_pyscss/__init__.py b/django_pyscss/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/django_pyscss/__init__.py
diff --git a/django_pyscss/scss.py b/django_pyscss/scss.py
new file mode 100644
index 0000000..9837436
--- /dev/null
+++ b/django_pyscss/scss.py
@@ -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
diff --git a/manage.py b/manage.py
new file mode 100755
index 0000000..eaec3ca
--- /dev/null
+++ b/manage.py
@@ -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)
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..c0da811
--- /dev/null
+++ b/setup.py
@@ -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',
+)
diff --git a/testproject/__init__.py b/testproject/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testproject/__init__.py
diff --git a/testproject/runtests.py b/testproject/runtests.py
new file mode 100644
index 0000000..394f24a
--- /dev/null
+++ b/testproject/runtests.py
@@ -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))
diff --git a/testproject/testapp1/__init__.py b/testproject/testapp1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testproject/testapp1/__init__.py
diff --git a/testproject/testapp1/admin.py b/testproject/testapp1/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/testproject/testapp1/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/testproject/testapp1/models.py b/testproject/testapp1/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/testproject/testapp1/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/testproject/testapp1/static/css/app1.scss b/testproject/testapp1/static/css/app1.scss
new file mode 100644
index 0000000..bbc87de
--- /dev/null
+++ b/testproject/testapp1/static/css/app1.scss
@@ -0,0 +1,3 @@
+.app1 {
+ color: #ff9900;
+}
diff --git a/testproject/testapp1/tests.py b/testproject/testapp1/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/testproject/testapp1/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/testproject/testapp1/views.py b/testproject/testapp1/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/testproject/testapp1/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/testproject/testapp2/__init__.py b/testproject/testapp2/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testproject/testapp2/__init__.py
diff --git a/testproject/testapp2/admin.py b/testproject/testapp2/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/testproject/testapp2/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/testproject/testapp2/models.py b/testproject/testapp2/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/testproject/testapp2/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/testproject/testapp2/static/css/app2.scss b/testproject/testapp2/static/css/app2.scss
new file mode 100644
index 0000000..d19b910
--- /dev/null
+++ b/testproject/testapp2/static/css/app2.scss
@@ -0,0 +1,2 @@
+@import "css/foo.scss";
+@import "css/app1.scss";
diff --git a/testproject/testapp2/tests.py b/testproject/testapp2/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/testproject/testapp2/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/testproject/testapp2/views.py b/testproject/testapp2/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/testproject/testapp2/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/testproject/testproject/__init__.py b/testproject/testproject/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testproject/testproject/__init__.py
diff --git a/testproject/testproject/settings.py b/testproject/testproject/settings.py
new file mode 100644
index 0000000..6983250
--- /dev/null
+++ b/testproject/testproject/settings.py
@@ -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'),
+)
diff --git a/testproject/testproject/static/css/foo.scss b/testproject/testproject/static/css/foo.scss
new file mode 100644
index 0000000..97ff765
--- /dev/null
+++ b/testproject/testproject/static/css/foo.scss
@@ -0,0 +1,3 @@
+.foo {
+ color: #ff0000;
+}
diff --git a/testproject/testproject/urls.py b/testproject/testproject/urls.py
new file mode 100644
index 0000000..de3a3a9
--- /dev/null
+++ b/testproject/testproject/urls.py
@@ -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)),
+)
diff --git a/testproject/testproject/wsgi.py b/testproject/testproject/wsgi.py
new file mode 100644
index 0000000..c6a4e4e
--- /dev/null
+++ b/testproject/testproject/wsgi.py
@@ -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()
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/test_import.py b/tests/test_import.py
new file mode 100644
index 0000000..1fdab51
--- /dev/null
+++ b/tests/test_import.py
@@ -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