Skip to content

Commit 4345509

Browse files
committed
Move core functionality out of management command and into library
1 parent 185f838 commit 4345509

File tree

6 files changed

+225
-81
lines changed

6 files changed

+225
-81
lines changed

README.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,14 +178,55 @@ django-sass only depends on libsass (which provides pre-built wheels for Windows
178178
and Linux), and of course Django (any version).
179179

180180

181+
Programmatically Compiling Sass
182+
-------------------------------
183+
184+
You can also use `django-sass` in Python to programmatically compile the sass.
185+
This is useful for build scripts and static site generators.
186+
187+
```python
188+
from django_sass import compile_sass
189+
190+
# Compile scss and write to output file.
191+
compile_sass(
192+
inpath="/path/to/file.scss",
193+
outpath="/path/to/output.css",
194+
output_style="compressed",
195+
precision=8,
196+
source_map=True
197+
)
198+
```
199+
200+
For more advanced usage, you can specify additional sass search paths outside
201+
of your Django project by using the `include_paths` argument.
202+
203+
```python
204+
from django_sass import compile_sass, find_static_paths
205+
206+
# Get Django's static paths.
207+
dirs = find_static_paths()
208+
209+
# Add external paths.
210+
dirs.append("/external/path/")
211+
212+
# Compile scss and write to output file.
213+
compile_sass(
214+
inpath="/path/to/file.scss",
215+
outpath="/path/to/output.css",
216+
output_style="compressed",
217+
precision=8,
218+
source_map=True,
219+
include_paths=dirs,
220+
)
221+
```
222+
181223
Contributing
182224
------------
183225

184226
To set up a development environment, first check out this repository, create a
185227
venv, then:
186228

187229
```
188-
(myvenv)$ pip install -e ./
189230
(myvenv)$ pip install -r requirements-dev.txt
190231
```
191232

@@ -196,6 +237,12 @@ Before committing, run static analysis tools:
196237
(myvenv)$ mypy
197238
```
198239

240+
Then run the unit tests:
241+
242+
```
243+
(myvenv)$ pytest
244+
```
245+
199246

200247
Changelog
201248
---------

django_sass/__init__.py

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,123 @@
1-
__version__ = "0.2.0"
1+
from typing import Dict, List
2+
import os
3+
4+
from django.contrib.staticfiles.finders import get_finders
5+
import sass
6+
7+
8+
def find_static_paths() -> List[str]:
9+
"""
10+
Finds all static paths available in this Django project.
11+
12+
:returns:
13+
List of paths containing static files.
14+
"""
15+
found_paths = []
16+
for finder in get_finders():
17+
if hasattr(finder, "storages"):
18+
for appname in finder.storages:
19+
if hasattr(finder.storages[appname], "location"):
20+
abspath = finder.storages[appname].location
21+
found_paths.append(abspath)
22+
return found_paths
23+
24+
25+
def find_static_scss() -> List[str]:
26+
"""
27+
Finds all static scss files available in this Django project.
28+
29+
:returns:
30+
List of paths of static scss files.
31+
"""
32+
scss_files = []
33+
for finder in get_finders():
34+
for path, storage in finder.list([]):
35+
if path.endswith(".scss"):
36+
fullpath = finder.find(path)
37+
scss_files.append(fullpath)
38+
return scss_files
39+
40+
41+
def compile_sass(inpath: str, outpath: str, output_style: str = None, precision: int = None,
42+
source_map: bool = False, include_paths: List[str] = None) -> None:
43+
"""
44+
Calls sass.compile() within context of Django's known static file paths,
45+
and writes output CSS and/or sourcemaps to file.
46+
47+
:param str inpath:
48+
Path to SCSS file or directory of SCSS files.
49+
:param str outpath:
50+
Path to a CSS file or directory in which to write output. The path will
51+
be created if it does not exist.
52+
:param str output_style:
53+
Corresponds to `output_style` from sass package.
54+
:param int precision:
55+
Corresponds to `precision` from sass package.
56+
:param bool source_map:
57+
If True, write a source map along with the output CSS file.
58+
Only valid when `inpath` is a file.
59+
:returns:
60+
None
61+
"""
62+
63+
# If include paths are not specified, use Django static paths
64+
include_paths = include_paths or find_static_paths()
65+
66+
# Additional sass args that must be figured out.
67+
sassargs = {} # type: Dict[str, object]
68+
69+
# Handle input directories.
70+
if os.path.isdir(inpath):
71+
# Assume outpath is also a dir, or make it.
72+
if not os.path.exists(outpath):
73+
os.makedirs(outpath)
74+
if os.path.isdir(outpath):
75+
sassargs.update({"dirname": (inpath, outpath)})
76+
else:
77+
raise NotADirectoryError(
78+
"Output path must also be a directory when input path is a directory."
79+
)
80+
81+
# Handle input files.
82+
outfile = None
83+
if os.path.isfile(inpath):
84+
sassargs.update({"filename": inpath})
85+
if os.path.isdir(outpath):
86+
outfile = os.path.join(
87+
outpath, os.path.basename(inpath.replace(".scss", ".css"))
88+
)
89+
else:
90+
outfile = outpath
91+
if source_map:
92+
sassargs.update({"source_map_filename": outfile + ".map"})
93+
94+
# Compile the sass.
95+
rval = sass.compile(
96+
output_style=output_style,
97+
precision=precision,
98+
include_paths=include_paths,
99+
**sassargs,
100+
)
101+
102+
# Write output.
103+
# sass.compile() will return None if used with dirname.
104+
# If used with filename, it will return a string of file contents.
105+
if rval and outfile:
106+
# If we got a css and sourcemap tuple, write the sourcemap.
107+
if isinstance(rval, tuple):
108+
map_outfile = outfile + ".map"
109+
outfile_dir = os.path.dirname(map_outfile)
110+
if not os.path.exists(outfile_dir):
111+
os.makedirs(outfile_dir, exist_ok=True)
112+
file = open(map_outfile, "w", encoding="utf8")
113+
file.write(rval[1])
114+
file.close()
115+
rval = rval[0]
116+
117+
# Write the outputted css to file.
118+
outfile_dir = os.path.dirname(outfile)
119+
if not os.path.exists(outfile_dir):
120+
os.makedirs(outfile_dir, exist_ok=True)
121+
file = open(outfile, "w", encoding="utf8")
122+
file.write(rval)
123+
file.close()

django_sass/management/commands/sass.py

Lines changed: 31 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
import time
44

55
from django.core.management.base import BaseCommand
6-
from django.contrib.staticfiles.finders import get_finders
76
import sass
87

8+
from django_sass import compile_sass, find_static_scss
9+
910

1011
class Command(BaseCommand):
1112
help = "Runs libsass including all paths from STATICFILES_FINDERS."
@@ -51,109 +52,66 @@ def add_arguments(self, parser):
5152
help="Watch input path and re-generate css files when scss files are changed.",
5253
)
5354

54-
def compile_sass(self, outfile: str, **kwargs) -> None:
55-
rval = sass.compile(**kwargs)
56-
# sass.compile() will return None if used with dirname.
57-
# If used with filename, it will return a string of file contents.
58-
if rval and outfile:
59-
# If we got a css and sourcemap tuple, write the sourcemap.
60-
if isinstance(rval, tuple):
61-
map_outfile = outfile + ".map"
62-
outfile_dir = os.path.dirname(map_outfile)
63-
if not os.path.exists(outfile_dir):
64-
os.makedirs(outfile_dir, exist_ok=True)
65-
file = open(map_outfile, "w", encoding="utf8")
66-
file.write(rval[1])
67-
file.close()
68-
rval = rval[0]
69-
70-
# Write the outputted css to file.
71-
outfile_dir = os.path.dirname(outfile)
72-
if not os.path.exists(outfile_dir):
73-
os.makedirs(outfile_dir, exist_ok=True)
74-
file = open(outfile, "w", encoding="utf8")
75-
file.write(rval)
76-
file.close()
77-
7855
def handle(self, *args, **options) -> None:
7956
"""
8057
Finds all static paths used by the project, and runs sass
8158
including those paths.
8259
"""
83-
found_paths = []
84-
for finder in get_finders():
85-
if hasattr(finder, "storages"):
86-
for appname in finder.storages:
87-
if hasattr(finder.storages[appname], "location"):
88-
abspath = finder.storages[appname].location
89-
found_paths.append(abspath)
90-
91-
sassargs = {"output_style": options["t"], "precision": options["p"]}
92-
inpath = options["in"][0]
93-
outpath = options["out"][0]
94-
outfile = None
9560

96-
if found_paths:
97-
sassargs.update({"include_paths": found_paths})
98-
99-
if os.path.isdir(inpath):
100-
# Assume outpath is also a dir, or make it.
101-
if not os.path.exists(outpath):
102-
os.makedirs(outpath)
103-
if os.path.isdir(outpath):
104-
sassargs.update({"dirname": (inpath, outpath)})
105-
else:
106-
raise NotADirectoryError(
107-
"Output path must also be a directory when input path is a directory."
108-
)
109-
110-
if os.path.isfile(inpath):
111-
sassargs.update({"filename": inpath})
112-
if os.path.isdir(outpath):
113-
outfile = os.path.join(
114-
outpath, os.path.basename(inpath.replace(".scss", ".css"))
115-
)
116-
else:
117-
outfile = outpath
118-
if options["g"]:
119-
sassargs.update({"source_map_filename": outfile + ".map"})
61+
# Parse options.
62+
o_inpath = options["in"][0]
63+
o_outpath = options["out"][0]
64+
o_srcmap = options["g"]
65+
o_precision = options["p"]
66+
o_style = options["t"]
12067

12168
# Watch files for changes if specified.
12269
if options["watch"]:
12370
try:
12471
self.stdout.write("Watching...")
72+
73+
# Track list of files to watch and their modified time.
12574
watchfiles = {}
12675
while True:
12776
needs_updated = False
12877

12978
# Build/update list of ALL scss files in static paths.
130-
for finder in get_finders():
131-
for path, storage in finder.list([]):
132-
if path.endswith(".scss"):
133-
fullpath = finder.find(path)
134-
prev_mtime = watchfiles.get(fullpath, 0)
135-
curr_mtime = os.stat(fullpath).st_mtime
136-
if curr_mtime > prev_mtime:
137-
needs_updated = True
138-
watchfiles.update({fullpath: curr_mtime})
79+
for fullpath in find_static_scss():
80+
prev_mtime = watchfiles.get(fullpath, 0)
81+
curr_mtime = os.stat(fullpath).st_mtime
82+
if curr_mtime > prev_mtime:
83+
needs_updated = True
84+
watchfiles.update({fullpath: curr_mtime})
13985

14086
# Recompile the sass if needed.
14187
if needs_updated:
14288
# Catch compile errors to keep the watcher running.
14389
try:
144-
self.compile_sass(outfile, **sassargs)
90+
compile_sass(
91+
inpath=o_inpath,
92+
outpath=o_outpath,
93+
output_style=o_style,
94+
precision=o_precision,
95+
source_map=o_srcmap,
96+
)
14597
self.stdout.write("Updated files at %s" % time.time())
14698
except sass.CompileError as exc:
14799
self.stdout.write(str(exc))
148100

149101
# Go back to sleep.
150102
time.sleep(3)
151103

152-
except KeyboardInterrupt:
104+
except (KeyboardInterrupt, InterruptedError):
153105
self.stdout.write("Bye.")
154106
sys.exit(0)
155107

156108
# Write css.
157109
self.stdout.write("Writing css...")
158-
self.compile_sass(outfile, **sassargs)
110+
compile_sass(
111+
inpath=o_inpath,
112+
outpath=o_outpath,
113+
output_style=o_style,
114+
precision=o_precision,
115+
source_map=o_srcmap,
116+
)
159117
self.stdout.write("Done.")

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ ignore_missing_imports = True
88
[tool:pytest]
99
DJANGO_SETTINGS_MODULE = testproject.settings
1010
junit_family = xunit2
11-
addopts = --cov django_sass --cov-report html --cov-report xml --junitxml junit/test-results.xml
11+
addopts = --cov django_sass --cov-report html --cov-report xml --junitxml junit/test-results.xml ./testproject/
1212
python_files = tests.py test_*.py
1313
filterwarnings =
1414
ignore

setup.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import os
22
from setuptools import setup, find_packages
33

4-
from django_sass import __version__
5-
64

75
with open(os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf8") as readme:
86
README = readme.read()
97

108
setup(
119
name="django-sass",
12-
version=__version__,
10+
version="0.2.0",
1311
author="CodeRed LLC",
1412
author_email="[email protected]",
1513
url="https://github.com/coderedcorp/django-sass",

0 commit comments

Comments
 (0)