Reworked the relative/absolute import system.

Now, it works more like HTML URL resolution, where any URL is relative
unless it starts with a /.
This commit is contained in:
Rocky Meza
2014-02-03 17:54:27 -07:00
parent ae411e0b97
commit 53d023e453
7 changed files with 84 additions and 52 deletions

View File

@@ -41,9 +41,7 @@ You can render SCSS from a file like this::
compiler.compile(scss_file='css/styles.scss') compiler.compile(scss_file='css/styles.scss')
The file needs to be able to be located by staticfiles finders in order to be The file needs to be able to be located by staticfiles finders in order to be
used. All imports are relative to the ``STATIC_ROOT``, but you can also have used.
relative imports from a file. If you prefix an import with ``./``, you can
import a sibling file without having to write out the whole import path.
.. class:: django_pyscss.scss.DjangoScss .. class:: django_pyscss.scss.DjangoScss

View File

@@ -3,6 +3,7 @@ from __future__ import absolute_import
import os import os
from compressor.filters import FilterBase from compressor.filters import FilterBase
from compressor.conf import settings
from django_pyscss.scss import DjangoScss, config from django_pyscss.scss import DjangoScss, config
@@ -14,8 +15,18 @@ class DjangoScssFilter(FilterBase):
# It looks like there is a bug in django-compressor because it expects # It looks like there is a bug in django-compressor because it expects
# us to accept attrs. # us to accept attrs.
super(DjangoScssFilter, self).__init__(content, filter_type, filename) super(DjangoScssFilter, self).__init__(content, filter_type, filename)
try:
# this is a link tag which means there is an SCSS file being
# referenced.
href = attrs['href']
except KeyError:
# this is a style tag which means this is inline SCSS.
self.relative_to = None
else:
self.relative_to = os.path.dirname(href.replace(settings.STATIC_URL, ''))
def input(self, **kwargs): def input(self, **kwargs):
if not os.path.exists(config.ASSETS_ROOT): if not os.path.exists(config.ASSETS_ROOT):
os.makedirs(config.ASSETS_ROOT) os.makedirs(config.ASSETS_ROOT)
return self.compiler.compile(self.content) return self.compiler.compile(scss_string=self.content,
relative_to=self.relative_to)

View File

@@ -4,7 +4,6 @@ import os
from django.contrib.staticfiles.storage import staticfiles_storage from django.contrib.staticfiles.storage import staticfiles_storage
from django.conf import settings from django.conf import settings
from django.core.exceptions import SuspiciousFileOperation
from scss import ( from scss import (
Scss, dequote, log, SourceFile, SassRule, config, Scss, dequote, log, SourceFile, SassRule, config,
@@ -48,26 +47,39 @@ class DjangoScss(Scss):
else: else:
return self.get_file_from_storage(filename) return self.get_file_from_storage(filename)
def _find_source_file(self, name): def get_possible_import_paths(self, filename, relative_to=None):
file_and_storage = self.get_file_and_storage(name) """
if file_and_storage is None: Returns an iterable of possible filenames for an import.
return None
else:
full_filename, storage = file_and_storage
if name not in self.source_files:
with storage.open(full_filename) as f:
source = f.read()
source_file = SourceFile( relative_to is None in the case that the SCSS is being rendered from a
full_filename, string or if it is the first file.
source, """
) if filename.startswith('/'): # absolute import
# SourceFile.__init__ calls os.path.realpath on this, we don't want filename = filename[1:]
# that. elif relative_to: # relative import
source_file.parent_dir = os.path.dirname(name) filename = os.path.join(relative_to, filename)
self.source_files.append(source_file)
self.source_file_index[full_filename] = source_file return [filename]
return self.source_file_index[full_filename]
def _find_source_file(self, filename, relative_to=None):
for name in self.get_possible_import_paths(filename, relative_to):
file_and_storage = self.get_file_and_storage(name)
if file_and_storage:
full_filename, storage = file_and_storage
if name not in self.source_files:
with storage.open(full_filename) as f:
source = f.read()
source_file = SourceFile(
full_filename,
source,
)
# SourceFile.__init__ calls os.path.realpath on this, we don't want
# that, we want them to remain relative.
source_file.parent_dir = os.path.dirname(name)
self.source_files.append(source_file)
self.source_file_index[full_filename] = source_file
return self.source_file_index[full_filename]
def _do_import(self, rule, scope, block): def _do_import(self, rule, scope, block):
""" """
@@ -83,10 +95,8 @@ class DjangoScss(Scss):
for name in names: for name in names:
name = dequote(name.strip()) name = dequote(name.strip())
if name.startswith('./'): relative_to = rule.source_file.parent_dir
name = rule.source_file.parent_dir + name[1:] source_file = self._find_source_file(name, relative_to)
source_file = self._find_source_file(name)
if source_file is None: if source_file is None:
i_codestr = self._do_magic_import(rule, scope, block) i_codestr = self._do_magic_import(rule, scope, block)
@@ -121,10 +131,12 @@ class DjangoScss(Scss):
rule.namespace.add_import(import_key, rule.import_key, rule.file_and_line) rule.namespace.add_import(import_key, rule.import_key, rule.file_and_line)
self.manage_children(_rule, scope) self.manage_children(_rule, scope)
def Compilation(self, scss_string=None, scss_file=None, super_selector=None, filename=None, is_sass=None, line_numbers=True): def Compilation(self, scss_string=None, scss_file=None, super_selector=None,
filename=None, is_sass=None, line_numbers=True,
relative_to=None):
""" """
Overwritten to call _find_source_file instead of Overwritten to call _find_source_file instead of
SourceFile.from_filename. SourceFile.from_filename. Also added the relative_to option.
""" """
if super_selector: if super_selector:
self.super_selector = super_selector + ' ' self.super_selector = super_selector + ' '
@@ -133,6 +145,9 @@ class DjangoScss(Scss):
source_file = None source_file = None
if scss_string is not None: if scss_string is not None:
source_file = SourceFile.from_string(scss_string, filename, is_sass, line_numbers) source_file = SourceFile.from_string(scss_string, filename, is_sass, line_numbers)
# Set the parent_dir to be something meaningful instead of the
# current working directory, which is never correct for DjangoScss.
source_file.parent_dir = relative_to
elif scss_file is not None: elif scss_file is not None:
# Call _find_source_file instead of SourceFile.from_filename # Call _find_source_file instead of SourceFile.from_filename
source_file = self._find_source_file(scss_file) source_file = self._find_source_file(scss_file)

View File

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

View File

@@ -1 +1 @@
@import "./foo.scss"; @import "foo.scss";

View File

@@ -9,8 +9,23 @@ APP2_LINK_TAG = """
{% endcompress %} {% endcompress %}
""" """
IMPORT_APP2_STYLE_TAG = """
{% load staticfiles compress %}
{% compress css %}
<style type="text/x-scss">
@import "css/app2.scss";
</style>
{% endcompress %}
"""
class CompressorTest(TestCase): class CompressorTest(TestCase):
def test_compressor_can_compile_scss(self): def test_compressor_can_compile_scss(self):
actual = Template(APP2_LINK_TAG).render(Context()) actual = Template(APP2_LINK_TAG).render(Context())
# 4b368862ec8c is the cache key that compressor gives to the compiled
# version of app2.scss.
self.assertIn('4b368862ec8c.css', actual)
def test_compressor_can_compile_scss_from_style_tag(self):
actual = Template(IMPORT_APP2_STYLE_TAG).render(Context())
self.assertIn('4b368862ec8c.css', actual) self.assertIn('4b368862ec8c.css', actual)

View File

@@ -9,26 +9,12 @@ from django_pyscss.scss import DjangoScss
from tests.utils import clean_css, CollectStaticTestCase from tests.utils import clean_css, CollectStaticTestCase
IMPORT_FOO = """
@import "css/foo.scss";
"""
with open(os.path.join(settings.BASE_DIR, 'testproject', 'static', 'css', 'foo.scss')) as f: with open(os.path.join(settings.BASE_DIR, 'testproject', 'static', 'css', 'foo.scss')) as f:
FOO_CONTENTS = f.read() 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: with open(os.path.join(settings.BASE_DIR, 'testapp1', 'static', 'css', 'app1.scss')) as f:
APP1_CONTENTS = f.read() APP1_CONTENTS = f.read()
IMPORT_APP2 = """
@import "css/app2.scss";
"""
APP2_CONTENTS = FOO_CONTENTS + APP1_CONTENTS APP2_CONTENTS = FOO_CONTENTS + APP1_CONTENTS
@@ -43,20 +29,27 @@ class CompilerTestMixin(object):
class ImportTestMixin(CompilerTestMixin): class ImportTestMixin(CompilerTestMixin):
def test_import_from_staticfiles_dirs(self): def test_import_from_staticfiles_dirs(self):
actual = self.compiler.compile(scss_string=IMPORT_FOO) actual = self.compiler.compile(scss_string='@import "/css/foo.scss";')
self.assertEqual(clean_css(actual), clean_css(FOO_CONTENTS))
def test_import_from_staticfiles_dirs_relative(self):
actual = self.compiler.compile(scss_string='@import "css/foo.scss";')
self.assertEqual(clean_css(actual), clean_css(FOO_CONTENTS)) self.assertEqual(clean_css(actual), clean_css(FOO_CONTENTS))
def test_import_from_app(self): def test_import_from_app(self):
actual = self.compiler.compile(scss_string=IMPORT_APP1) actual = self.compiler.compile(scss_string='@import "/css/app1.scss";')
self.assertEqual(clean_css(actual), clean_css(APP1_CONTENTS))
def test_import_from_app_relative(self):
actual = self.compiler.compile(scss_string='@import "css/app1.scss";')
self.assertEqual(clean_css(actual), clean_css(APP1_CONTENTS)) self.assertEqual(clean_css(actual), clean_css(APP1_CONTENTS))
def test_imports_within_file(self): def test_imports_within_file(self):
actual = self.compiler.compile(scss_string=IMPORT_APP2) actual = self.compiler.compile(scss_string='@import "/css/app2.scss";')
self.assertEqual(clean_css(actual), clean_css(APP2_CONTENTS)) self.assertEqual(clean_css(actual), clean_css(APP2_CONTENTS))
def test_relative_import(self): def test_relative_import(self):
bar_scss = 'css/bar.scss' actual = self.compiler.compile(scss_file='/css/bar.scss')
actual = self.compiler.compile(scss_file=bar_scss)
self.assertEqual(clean_css(actual), clean_css(FOO_CONTENTS)) self.assertEqual(clean_css(actual), clean_css(FOO_CONTENTS))
def test_bad_import(self): def test_bad_import(self):