diff options
author | Rocky Meza <rocky@fusionbox.com> | 2014-02-01 18:22:17 -0700 |
---|---|---|
committer | Rocky Meza <rocky@fusionbox.com> | 2014-02-01 19:05:04 -0700 |
commit | 2a53a7abd1ddba02c44b81a24305b03532b45fee (patch) | |
tree | 6b009211aa1eb8355464dcf0e765b076207fa778 | |
download | django-pyscss-2a53a7abd1ddba02c44b81a24305b03532b45fee.tar.gz |
django-pyscss - use PySCSS in Django more easily
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/ @@ -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 |