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')
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
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.
used.
.. class:: django_pyscss.scss.DjangoScss

View File

@ -3,6 +3,7 @@ from __future__ import absolute_import
import os
from compressor.filters import FilterBase
from compressor.conf import settings
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
# us to accept attrs.
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):
if not os.path.exists(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.conf import settings
from django.core.exceptions import SuspiciousFileOperation
from scss import (
Scss, dequote, log, SourceFile, SassRule, config,
@ -48,26 +47,39 @@ class DjangoScss(Scss):
else:
return self.get_file_from_storage(filename)
def _find_source_file(self, name):
file_and_storage = self.get_file_and_storage(name)
if file_and_storage is None:
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()
def get_possible_import_paths(self, filename, relative_to=None):
"""
Returns an iterable of possible filenames for an import.
source_file = SourceFile(
full_filename,
source,
)
# SourceFile.__init__ calls os.path.realpath on this, we don't want
# that.
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]
relative_to is None in the case that the SCSS is being rendered from a
string or if it is the first file.
"""
if filename.startswith('/'): # absolute import
filename = filename[1:]
elif relative_to: # relative import
filename = os.path.join(relative_to, filename)
return [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):
"""
@ -83,10 +95,8 @@ class DjangoScss(Scss):
for name in names:
name = dequote(name.strip())
if name.startswith('./'):
name = rule.source_file.parent_dir + name[1:]
source_file = self._find_source_file(name)
relative_to = rule.source_file.parent_dir
source_file = self._find_source_file(name, relative_to)
if source_file is None:
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)
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
SourceFile.from_filename.
SourceFile.from_filename. Also added the relative_to option.
"""
if super_selector:
self.super_selector = super_selector + ' '
@ -133,6 +145,9 @@ class DjangoScss(Scss):
source_file = None
if scss_string is not None:
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:
# Call _find_source_file instead of SourceFile.from_filename
source_file = self._find_source_file(scss_file)

View File

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

View File

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

View File

@ -9,8 +9,23 @@ APP2_LINK_TAG = """
{% endcompress %}
"""
IMPORT_APP2_STYLE_TAG = """
{% load staticfiles compress %}
{% compress css %}
<style type="text/x-scss">
@import "css/app2.scss";
</style>
{% endcompress %}
"""
class CompressorTest(TestCase):
def test_compressor_can_compile_scss(self):
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)

View File

@ -9,26 +9,12 @@ from django_pyscss.scss import DjangoScss
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:
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
@ -43,20 +29,27 @@ class CompilerTestMixin(object):
class ImportTestMixin(CompilerTestMixin):
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))
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))
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))
def test_relative_import(self):
bar_scss = 'css/bar.scss'
actual = self.compiler.compile(scss_file=bar_scss)
actual = self.compiler.compile(scss_file='/css/bar.scss')
self.assertEqual(clean_css(actual), clean_css(FOO_CONTENTS))
def test_bad_import(self):