Skip to content

Commit 53d023e

Browse files
author
Rocky Meza
committed
Reworked the relative/absolute import system.
Now, it works more like HTML URL resolution, where any URL is relative unless it starts with a /.
1 parent ae411e0 commit 53d023e

File tree

7 files changed

+85
-53
lines changed

7 files changed

+85
-53
lines changed

README.rst

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@ You can render SCSS from a file like this::
4141
compiler.compile(scss_file='css/styles.scss')
4242

4343
The file needs to be able to be located by staticfiles finders in order to be
44-
used. All imports are relative to the ``STATIC_ROOT``, but you can also have
45-
relative imports from a file. If you prefix an import with ``./``, you can
46-
import a sibling file without having to write out the whole import path.
44+
used.
4745

4846

4947
.. class:: django_pyscss.scss.DjangoScss

django_pyscss/compressor.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44

55
from compressor.filters import FilterBase
6+
from compressor.conf import settings
67

78
from django_pyscss.scss import DjangoScss, config
89

@@ -14,8 +15,18 @@ def __init__(self, content, attrs=None, filter_type=None, filename=None):
1415
# It looks like there is a bug in django-compressor because it expects
1516
# us to accept attrs.
1617
super(DjangoScssFilter, self).__init__(content, filter_type, filename)
18+
try:
19+
# this is a link tag which means there is an SCSS file being
20+
# referenced.
21+
href = attrs['href']
22+
except KeyError:
23+
# this is a style tag which means this is inline SCSS.
24+
self.relative_to = None
25+
else:
26+
self.relative_to = os.path.dirname(href.replace(settings.STATIC_URL, ''))
1727

1828
def input(self, **kwargs):
1929
if not os.path.exists(config.ASSETS_ROOT):
2030
os.makedirs(config.ASSETS_ROOT)
21-
return self.compiler.compile(self.content)
31+
return self.compiler.compile(scss_string=self.content,
32+
relative_to=self.relative_to)

django_pyscss/scss.py

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from django.contrib.staticfiles.storage import staticfiles_storage
66
from django.conf import settings
7-
from django.core.exceptions import SuspiciousFileOperation
87

98
from scss import (
109
Scss, dequote, log, SourceFile, SassRule, config,
@@ -48,26 +47,39 @@ def get_file_and_storage(self, filename):
4847
else:
4948
return self.get_file_from_storage(filename)
5049

51-
def _find_source_file(self, name):
52-
file_and_storage = self.get_file_and_storage(name)
53-
if file_and_storage is None:
54-
return None
55-
else:
56-
full_filename, storage = file_and_storage
57-
if name not in self.source_files:
58-
with storage.open(full_filename) as f:
59-
source = f.read()
60-
61-
source_file = SourceFile(
62-
full_filename,
63-
source,
64-
)
65-
# SourceFile.__init__ calls os.path.realpath on this, we don't want
66-
# that.
67-
source_file.parent_dir = os.path.dirname(name)
68-
self.source_files.append(source_file)
69-
self.source_file_index[full_filename] = source_file
70-
return self.source_file_index[full_filename]
50+
def get_possible_import_paths(self, filename, relative_to=None):
51+
"""
52+
Returns an iterable of possible filenames for an import.
53+
54+
relative_to is None in the case that the SCSS is being rendered from a
55+
string or if it is the first file.
56+
"""
57+
if filename.startswith('/'): # absolute import
58+
filename = filename[1:]
59+
elif relative_to: # relative import
60+
filename = os.path.join(relative_to, filename)
61+
62+
return [filename]
63+
64+
def _find_source_file(self, filename, relative_to=None):
65+
for name in self.get_possible_import_paths(filename, relative_to):
66+
file_and_storage = self.get_file_and_storage(name)
67+
if file_and_storage:
68+
full_filename, storage = file_and_storage
69+
if name not in self.source_files:
70+
with storage.open(full_filename) as f:
71+
source = f.read()
72+
73+
source_file = SourceFile(
74+
full_filename,
75+
source,
76+
)
77+
# SourceFile.__init__ calls os.path.realpath on this, we don't want
78+
# that, we want them to remain relative.
79+
source_file.parent_dir = os.path.dirname(name)
80+
self.source_files.append(source_file)
81+
self.source_file_index[full_filename] = source_file
82+
return self.source_file_index[full_filename]
7183

7284
def _do_import(self, rule, scope, block):
7385
"""
@@ -83,10 +95,8 @@ def _do_import(self, rule, scope, block):
8395
for name in names:
8496
name = dequote(name.strip())
8597

86-
if name.startswith('./'):
87-
name = rule.source_file.parent_dir + name[1:]
88-
89-
source_file = self._find_source_file(name)
98+
relative_to = rule.source_file.parent_dir
99+
source_file = self._find_source_file(name, relative_to)
90100

91101
if source_file is None:
92102
i_codestr = self._do_magic_import(rule, scope, block)
@@ -121,10 +131,12 @@ def _do_import(self, rule, scope, block):
121131
rule.namespace.add_import(import_key, rule.import_key, rule.file_and_line)
122132
self.manage_children(_rule, scope)
123133

124-
def Compilation(self, scss_string=None, scss_file=None, super_selector=None, filename=None, is_sass=None, line_numbers=True):
134+
def Compilation(self, scss_string=None, scss_file=None, super_selector=None,
135+
filename=None, is_sass=None, line_numbers=True,
136+
relative_to=None):
125137
"""
126138
Overwritten to call _find_source_file instead of
127-
SourceFile.from_filename.
139+
SourceFile.from_filename. Also added the relative_to option.
128140
"""
129141
if super_selector:
130142
self.super_selector = super_selector + ' '
@@ -133,6 +145,9 @@ def Compilation(self, scss_string=None, scss_file=None, super_selector=None, fil
133145
source_file = None
134146
if scss_string is not None:
135147
source_file = SourceFile.from_string(scss_string, filename, is_sass, line_numbers)
148+
# Set the parent_dir to be something meaningful instead of the
149+
# current working directory, which is never correct for DjangoScss.
150+
source_file.parent_dir = relative_to
136151
elif scss_file is not None:
137152
# Call _find_source_file instead of SourceFile.from_filename
138153
source_file = self._find_source_file(scss_file)
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
@import "css/foo.scss";
2-
@import "css/app1.scss";
1+
@import "foo.scss";
2+
@import "app1.scss";
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
@import "./foo.scss";
1+
@import "foo.scss";

tests/test_compressor.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,23 @@
99
{% endcompress %}
1010
"""
1111

12+
IMPORT_APP2_STYLE_TAG = """
13+
{% load staticfiles compress %}
14+
{% compress css %}
15+
<style type="text/x-scss">
16+
@import "css/app2.scss";
17+
</style>
18+
{% endcompress %}
19+
"""
20+
1221

1322
class CompressorTest(TestCase):
1423
def test_compressor_can_compile_scss(self):
1524
actual = Template(APP2_LINK_TAG).render(Context())
25+
# 4b368862ec8c is the cache key that compressor gives to the compiled
26+
# version of app2.scss.
27+
self.assertIn('4b368862ec8c.css', actual)
28+
29+
def test_compressor_can_compile_scss_from_style_tag(self):
30+
actual = Template(IMPORT_APP2_STYLE_TAG).render(Context())
1631
self.assertIn('4b368862ec8c.css', actual)

tests/test_scss.py

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,12 @@
99
from tests.utils import clean_css, CollectStaticTestCase
1010

1111

12-
IMPORT_FOO = """
13-
@import "css/foo.scss";
14-
"""
15-
1612
with open(os.path.join(settings.BASE_DIR, 'testproject', 'static', 'css', 'foo.scss')) as f:
1713
FOO_CONTENTS = f.read()
1814

19-
20-
IMPORT_APP1 = """
21-
@import "css/app1.scss";
22-
"""
23-
2415
with open(os.path.join(settings.BASE_DIR, 'testapp1', 'static', 'css', 'app1.scss')) as f:
2516
APP1_CONTENTS = f.read()
2617

27-
28-
IMPORT_APP2 = """
29-
@import "css/app2.scss";
30-
"""
31-
3218
APP2_CONTENTS = FOO_CONTENTS + APP1_CONTENTS
3319

3420

@@ -43,20 +29,27 @@ def setUp(self):
4329

4430
class ImportTestMixin(CompilerTestMixin):
4531
def test_import_from_staticfiles_dirs(self):
46-
actual = self.compiler.compile(scss_string=IMPORT_FOO)
32+
actual = self.compiler.compile(scss_string='@import "/css/foo.scss";')
33+
self.assertEqual(clean_css(actual), clean_css(FOO_CONTENTS))
34+
35+
def test_import_from_staticfiles_dirs_relative(self):
36+
actual = self.compiler.compile(scss_string='@import "css/foo.scss";')
4737
self.assertEqual(clean_css(actual), clean_css(FOO_CONTENTS))
4838

4939
def test_import_from_app(self):
50-
actual = self.compiler.compile(scss_string=IMPORT_APP1)
40+
actual = self.compiler.compile(scss_string='@import "/css/app1.scss";')
41+
self.assertEqual(clean_css(actual), clean_css(APP1_CONTENTS))
42+
43+
def test_import_from_app_relative(self):
44+
actual = self.compiler.compile(scss_string='@import "css/app1.scss";')
5145
self.assertEqual(clean_css(actual), clean_css(APP1_CONTENTS))
5246

5347
def test_imports_within_file(self):
54-
actual = self.compiler.compile(scss_string=IMPORT_APP2)
48+
actual = self.compiler.compile(scss_string='@import "/css/app2.scss";')
5549
self.assertEqual(clean_css(actual), clean_css(APP2_CONTENTS))
5650

5751
def test_relative_import(self):
58-
bar_scss = 'css/bar.scss'
59-
actual = self.compiler.compile(scss_file=bar_scss)
52+
actual = self.compiler.compile(scss_file='/css/bar.scss')
6053
self.assertEqual(clean_css(actual), clean_css(FOO_CONTENTS))
6154

6255
def test_bad_import(self):

0 commit comments

Comments
 (0)