pax_global_header00006660000000000000000000000064143255656150014526gustar00rootroot0000000000000052 comment=2639eeda8722969b4286f8f95d5e7d41fd8fb6f2 django-sass-processor-1.2.2/000077500000000000000000000000001432556561500157565ustar00rootroot00000000000000django-sass-processor-1.2.2/.github/000077500000000000000000000000001432556561500173165ustar00rootroot00000000000000django-sass-processor-1.2.2/.github/workflows/000077500000000000000000000000001432556561500213535ustar00rootroot00000000000000django-sass-processor-1.2.2/.github/workflows/publish.yml000066400000000000000000000016041432556561500235450ustar00rootroot00000000000000name: Publish django-sass-processor on: push: tags: - '*' jobs: publish: name: "Publish release" runs-on: "ubuntu-latest" environment: name: deploy strategy: matrix: python-version: ["3.9"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install build --user - name: Build 🐍 Python 📦 Package run: python -m build --sdist --wheel --outdir dist/ - name: Publish 🐍 Python 📦 Package to PyPI if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@master with: password: ${{ secrets.PYPI_API_TOKEN_SASS_PROCESSOR }} django-sass-processor-1.2.2/.github/workflows/tests.yml000066400000000000000000000017231432556561500232430ustar00rootroot00000000000000name: Django CI on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.7", "3.8", "3.9", "3.10"] django-version: ["2.2", "3.2", "4.0"] exclude: - python-version: 3.9 django-version: 2.2 - python-version: 3.10 django-version: 2.2 - python-version: 3.7 django-version: 4.0 steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Install Dependencies run: | python -m pip install --upgrade pip pip install "Django==${{ matrix.django-version }}.*" pip install -r tests/requirements.txt python setup.py install - name: Test with pytest run: | python -m pytest tests django-sass-processor-1.2.2/.gitignore000066400000000000000000000044451432556561500177550ustar00rootroot00000000000000# Created by https://www.gitignore.io/api/macos,linux,python,django,windows ### Django ### *.log *.pot *.pyc __pycache__/ local_settings.py db.sqlite3 media .pytest_cache ### Linux ### *~ # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* # .nfs files are created when an open file is removed but is still being accessed .nfs* ### macOS ### *.DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### Python ### # Byte-compiled / optimized / DLL files *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo # Django stuff: # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site ### Windows ### # Windows thumbnail cache files Thumbs.db ehthumbs.db ehthumbs_vista.db # Folder config file Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msm *.msp # Windows shortcuts *.lnk # End of https://www.gitignore.io/api/macos,linux,python,django,windows django-sass-processor-1.2.2/CHANGELOG.md000066400000000000000000000112371432556561500175730ustar00rootroot00000000000000# Changes for django-sass-processor - 1.2.2 * Revert regression introduced in version 1.1: Remove compatibility layer to support Django's `ManifestStaticFilesStorage`. - 1.2.1 * In newer versions of Django `default_app_config`is deprecated. - 1.2 * Stringify directory settings, since they might use Python's `pathlib.Path` class. * Add support for Django-4.0. - 1.1 * Add compatibility layer to support Django's `ManifestStaticFilesStorage`. - 1.0.1 * Fix storage options for non-filesystem static storages (thanks to TheRandomDog for finding) * Dev: Move to setup.cfg configuration - 1.0.0 * Management command `compilescss` now uses the same storage as the template tags. * Any storage can now be used as destination. * Breaking change: The argument `--use-processor-root` to `compilescss` was replaced with `--use-storage`. * Breaking change: `SassS3Boto3Storage` was removed. Use the `S3Boto3Storage` from django-storages directly. * Breaking change: Suppor for Django <2.2 was dropped * Dev: Migrated setup meta-data to setup.cfg * Dev: Enabled tests on Python 3.9 - 0.8.2 * Fixes: Management command `find_sources` does not ignore `SASS_PROCESSOR_AUTO_INCLUDE`. - 0.8.1 * Add support for Django-3.1. - 0.8 * Add support for Django-3.0. * Drop support for Python<3. - 0.7.5 * Latest version to support Python-2.7. Tested with Django-1.9, Django-1.10, Django-1.11, Django-2.0 Django-2.1 and Django-2.2 using Python-3.5...3.7. - 0.7.4 * Prevent the warnings about `Found another file with the destination path ...`, while running `./manage.py collectstatic`. - 0.7.3 * In managment command `compilescss`, also catch `IndentionError` of parsed files. - 0.7.2 * Prevent empty content when using autoprefixer. * Source Map is now using relative paths. This fixes the path naming problems on Windows platforms. - 0.7.1 * Source Map is now using relative paths. This fixes the path naming problems on Windows platforms. - 0.7 * Allow to call directly into Python functions. - 0.6 * Add autoprefixing via external postcss. - 0.5.8 * _Potentially Breaking_: `libsass` is not autoinstalled as the dependency anymore. * Add support for Django-2.0. - 0.5.7 * Fixed: Catch exception if s3boto is not installed. - 0.5.6 * Added compatibility layer to work with AWS S3 Storage. - 0.5.5 * Create directory `SASS_PROCESSOR_ROOT` if it does not exist. - 0.5.4 * Added unit tests and continuous integration to the project. - 0.5.3 * Fixed compilescss: Did not find calls of sass_processor within a dict, list or tuple - 0.5.2 * Fixed Python 3 incompatibility. Open files as binaries, since they may contain unicode characters. - 0.5.1 * Add `APPS_INCLUDE_DIRS` to the SASS include path. - 0.5.0 * SASS/SCSS files can also be referenced in pure Python files, for instance in `Media` class or `media` property definitions. * The SASS processor will look for potential include directories, so that the `@import "..."` statement also works for SASS files located in other Django apps. - 0.4.0 - 0.4.4 * Refactored the sass processor into a self-contained class `SassProcessor`, which can be accessed through an API, the Jinja2 template engine and the existing templatetag. - 0.3.5 * Added Jinja2 support, see [Jinja2 support](#jinja2-support). - 0.3.4 * Fixed: `get_template_sources()` in Django-1.9 returns Objects rather than strings. * In command, use `ArgumentParser` rather than `OptionParser`. - 0.3.1...0.3.3 * Changed the build process in `setup.py`. - 0.3.0 * Compatible with Django 1.8+. * bootstrap3-sass ready: appropriate floating point precision (8) can be set in `settings.py`. * Offline compilation results may optionally be stored in `SASS_PROCESSOR_ROOT`. - 0.2.6 * Hotfix: added SASS function `get-setting` also to offline compiler. - 0.2.5 * Compatible with Python3 * Replaced `SortedDict` with `OrderedDict` to be prepared for Django-1.9 * Raise a `TemplateSyntax` error, if a SASS `@include "..."` fails to find the file. * Added SASS function `get-setting` to fetch configuration directives from `settings.py`. - 0.2.4 * Forcing compiled unicode to bytes, since 'Font Awesome' uses Unicode Private Use Area (PUA) and hence implicit conversion on `fh.write()` failed. - 0.2.3 * Allow for setting template extensions and output style. * Force Django to calculate template_source_loaders from TEMPLATE_LOADERS settings, by asking to find a dummy template. - 0.2.0 * Removed dependency to **django-sekizai** and **django-classy-tags**. It now can operate in stand-alone mode. Therefore the project has been renamed to **django-sass-processor**. - 0.1.0 * Initial revision named **django-sekizai-processors**, based on a preprocessor for the Sekizai template tags `{% addtoblock %}`. django-sass-processor-1.2.2/LICENSE-MIT000066400000000000000000000021651432556561500174160ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Jacob Rief Copyright (c) 2021 Dominik George Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. django-sass-processor-1.2.2/MANIFEST.in000066400000000000000000000000361432556561500175130ustar00rootroot00000000000000recursive-include tests/ *.py django-sass-processor-1.2.2/README.md000066400000000000000000000413421432556561500172410ustar00rootroot00000000000000# django-sass-processor Annoyed having to run a Compass, Grunt or Gulp daemon while developing Django projects? Well, then this app is for you! Compile SASS/SCSS files on the fly without having to manage third party services nor special IDE plugins. [![Build Status](https://github.com/jrief/django-sass-processor/actions/workflows/tests.yml/badge.svg)](https://github.com/jrief/django-sass-processor/actions) [![PyPI](https://img.shields.io/pypi/pyversions/django-sass-processor.svg)]() [![PyPI version](https://img.shields.io/pypi/v/django-sass-processor.svg)](https://pypi.python.org/pypi/django-sass-processor) [![PyPI](https://img.shields.io/pypi/l/django-sass-processor.svg)]() [![Downloads](https://img.shields.io/pypi/dm/django-sass-processor.svg)](https://pypi.python.org/pypi/django-sass-processor) [![Twitter Follow](https://img.shields.io/twitter/follow/shields_io.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/jacobrief) ## Other good reasons for using this library * Refer SASS/SCSS files directly from your sources, instead of referring a compiled CSS file, having to rely on another utility which creates them from SASS/SCSS files, hidden in your source tree. * Use Django's `settings.py` for the configuration of paths, box sizes etc., instead of having another SCSS specific file (typically `_variables.scss`), to hold these. * Extend your SASS functions by calling Python functions directly out of your Django project. * View SCSS errors directly in the debug console of your Django's development server. **django-sass-processor** converts `*.scss` or `*.sass` files into `*.css` while rendering templates. For performance reasons this is done only once, since the preprocessor keeps track on the timestamps and only recompiles, if any of the imported SASS/SCSS files is younger than the corresponding generated CSS file. ## Introduction This Django app provides a templatetag `{% sass_src 'path/to/file.scss' %}`, which can be used instead of the built-in templatetag `static`. This templatetag also works inside Jinja2 templates. If SASS/SCSS files shall be referenced through the `Media` class, or `media` property, the SASS processor can be used directly. Additionally, **django-sass-processor** is shipped with a management command, which can convert the content of all occurrences inside the templatetag `sass_src` as an offline operation. Hence the **libsass** compiler is not required in a production environment. During development, a [sourcemap](https://developer.chrome.com/devtools/docs/css-preprocessors) is generated along side with the compiled `*.css` file. This allows to debug style sheet errors much easier. With this tool, you can safely remove your Ruby installations "Compass" and "SASS" from your Django projects. You neither need any directory "watching" daemons based on node.js. ## Project's Home On GitHub: https://github.com/jrief/django-sass-processor Please use the issue tracker to report bugs or propose new features. ## Installation ``` pip install libsass django-compressor django-sass-processor ``` `django-compressor` is required only for offline compilation, when using the command `manage.py compilescss`. `libsass` is not required on the production environment, if SASS/SCSS files have been precompiled and deployed using offline compilation. ## Configuration In `settings.py` add to: ```python INSTALLED_APPS = [ ... 'sass_processor', ... ] ``` **django-sass-processor** is shipped with a special finder, to locate the generated `*.css` files in the directory referred by `SASS_PROCESSOR_ROOT` (or, if unset `STATIC_ROOT`). Just add it to your `settings.py`. If there is no `STATICFILES_FINDERS` in your `settings.py` don't forget to include the **Django** [default finders](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-STATICFILES_FINDERS). ```python STATICFILES_FINDERS = [ 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'sass_processor.finders.CssFinder', ... ] ``` Optionally, add a list of additional search paths, the SASS compiler may examine when using the `@import "...";` statement in SASS/SCSS files: ```python import os SASS_PROCESSOR_INCLUDE_DIRS = [ os.path.join(PROJECT_PATH, 'extra-styles/scss'), os.path.join(PROJECT_PATH, 'node_modules'), ] ``` Additionally, **django-sass-processor** will traverse all installed Django apps (`INSTALLED_APPS`) and look into their static folders. If any of them contain a file matching the regular expression pattern `^_.+\.(scss|sass)$` (read: filename starts with an underscore and is of type `scss` or `sass`), then that app specific static folder is added to the **libsass** include dirs. This feature can be disabled in your settings with: ```python SASS_PROCESSOR_AUTO_INCLUDE = False ``` If inside of your SASS/SCSS files, you also want to import (using `@import "path/to/scssfile";`) files which do not start with an underscore, then you can configure another Regex pattern in your settings, for instance: ```python SASS_PROCESSOR_INCLUDE_FILE_PATTERN = r'^.+\.scss$' ``` will look for all files of type `scss`. Remember that SASS/SCSS files which start with an underscore, are intended to be imported by other SASS/SCSS files, while files starting with a letter or number are intended to be included by the HTML tag ``. During development, or when `SASS_PROCESSOR_ENABLED = True`, the compiled file is placed into the folder referenced by `SASS_PROCESSOR_ROOT` (if unset, this setting defaults to `STATIC_ROOT`). Having a location outside of the working directory prevents to pollute your local `static/css/...` directories with auto-generated files. Therefore assure, that this directory is writable by the Django runserver. #### Fine tune SASS compiler parameters in `settings.py`. Integer `SASS_PRECISION` sets floating point precision for output css. libsass' default is `5`. Note: **bootstrap-sass** requires `8`, otherwise various layout problems _will_ occur. ```python SASS_PRECISION = 8 ``` `SASS_OUTPUT_STYLE` sets coding style of the compiled result, one of `compact`, `compressed`, `expanded`, or `nested`. Default is `nested` for `DEBUG` and `compressed` in production. Note: **libsass-python** 0.8.3 has [problem encoding result while saving on Windows](https://github.com/dahlia/libsass-python/pull/82), the issue is already fixed and will be included in future `pip` package release, in the meanwhile avoid `compressed` output style. ```python SASS_OUTPUT_STYLE = 'compact' ``` ### Jinja2 support `sass_processor.jinja2.ext.SassSrc` is a Jinja2 extension. Add it to your Jinja2 environment to enable the tag `sass_src`, there is no need for a `load` tag. Example of how to add your Jinja2 environment to Django: In `settings.py`: ```python TEMPLATES = [{ 'BACKEND': 'django.template.backends.jinja2.Jinja2', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'environment': 'yourapp.jinja2.environment' }, ... }] ``` Make sure to add the default template backend, if you're still using Django templates elsewhere. This is covered in the [Upgrading templates documentation](https://docs.djangoproject.com/en/stable/ref/templates/upgrading/). In `yourapp/jinja2.py`: ```python # Include this for Python 2. from __future__ import absolute_import from jinja2 import Environment def environment(**kwargs): extensions = [] if 'extensions' not in kwargs else kwargs['extensions'] extensions.append('sass_processor.jinja2.ext.SassSrc') kwargs['extensions'] = extensions return Environment(**kwargs) ``` If you want to make use of the `compilescss` command, then you will also have to add the following to your settings: ```python from yourapp.jinja2 import environment COMPRESS_JINJA2_GET_ENVIRONMENT = environment ``` ## Usage ### In your Django templates ```django {% load sass_tags %} ``` The above template code will be rendered as HTML ```html ``` You can safely use this templatetag inside a [Sekizai](https://django-sekizai.readthedocs.io/)'s `{% addtoblock "css" %}` statement. ### In Media classes or properties In Python code, you can access the API of the SASS processor directly. This for instance is useful in Django's admin or form framework. ```python from sass_processor.processor import sass_processor class SomeAdminOrFormClass(...): ... class Media: css = { 'all': [sass_processor('myapp/css/mystyle.scss')], } ``` ## Add vendor prefixes to CSS rules using values from https://caniuse.com/ Writing SCSS shall be fast and easy and you should not have to care, whether to add vendor specific prefixes to your CSS directives. Unfortunately there is no pure Python package to solve this, but with a few node modules, we can add this to our process chain. As superuser install ```shell npm install -g npx ``` and inside your project root, install ```shell npm install postcss-cli autoprefixer ``` Check that the path of `node_modules` corresponds to its entry in the settings directive `STATICFILES_DIRS` (see below). In case `npx` can not be found in your system path, use the settings directive `NODE_NPX_PATH = /path/to/npx` to point to that executable. If everything is setup correctly, **django-sass-processor** adds all required vendor prefixes to the compiled CSS files. For further information, refer to the [Autoprefixer](https://github.com/postcss/autoprefixer) package. To disable autoprefixing, set `NODE_NPX_PATH = None`. **Important note**: If `npx` is installed, but `postcss` and/or `autoprefixer` are missing in the local `node_modules`, setting `NODE_NPX_PATH` to `None` is manadatory, otherwise **django-sass-processor** does not know how to postprocess the generated CSS files. ## Offline compilation If you want to precompile all occurrences of your SASS/SCSS files for the whole project, on the command line invoke: ```shell ./manage.py compilescss ``` This is useful for preparing production environments, where SASS/SCSS files can't be compiled on the fly. To simplify the deployment, the compiled `*.css` files are stored side-by-side with their corresponding SASS/SCSS files. After compiling the files run ```shell ./manage.py collectstatic ``` as you would in a normal deployment. In case you don't want to expose the SASS/SCSS files in a production environment, deploy with: ```shell ./manage.py collectstatic --ignore=*.scss ``` To get rid of the compiled `*.css` files in your local static directories, simply reverse the above command: ```shell ./manage.py compilescss --delete-files ``` This will remove all occurrences of previously generated `*.css` files. Or you may compile results to the `SASS_PROCESSOR_ROOT` directory directy (if not specified - to `STATIC_ROOT`): ```shell ./manage.py compilescss --use-storage ``` Combine with `--delete-files` switch to purge results from there. If you use an alternative templating engine set its name in `--engine` argument. Currently `django` and `jinja2` are supported, see [django-compressor documentation](http://django-compressor.readthedocs.org/en/latest/) on how to set up `COMPRESS_JINJA2_GET_ENVIRONMENT` to configure jinja2 engine support. During offline compilation **django-sass-processor** parses all Python files and looks for invocations of `sass_processor('path/to/sassfile.scss')`. Therefore the string specifying the filename must be hard coded and shall not be concatenated or being somehow generated. ### Alternative templates By default, **django-sass-processor** will locate SASS/SCSS files from .html templates, but you can extend or override this behavior in your settings with: ```python SASS_TEMPLATE_EXTS = ['.html','.jade'] ``` ## Configure SASS variables through settings.py In SASS, a nasty problem is to set the correct include paths for icons and fonts. Normally this is done through a `_variables.scss` file, but this inhibits a configuration through your projects `settings.py`. To avoid the need for duplicate configuration settings, **django-sass-processor** offers a SASS function to fetch any arbitrary configuration directive from the project's `settings.py`. This is specially handy to set the include path of your Glyphicons font directory. Assume, Bootstrap-SASS has been installed using: ```shell npm install bootstrap-sass ``` then locate the directory named `node_modules` and add it to your settings, so that your fonts are accessible through the Django's `django.contrib.staticfiles.finders.FileSystemFinder`: ```python STATICFILES_DIRS = [ ... ('node_modules', '/path/to/your/project/node_modules/'), ... ] NODE_MODULES_URL = STATIC_URL + 'node_modules/' ``` With the SASS function `get-setting`, it is possible to override any SASS variable with a value configured in the project's `settings.py`. For the Glyphicons font search path, add this to your `_variables.scss`: ```scss $icon-font-path: unquote(get-setting(NODE_MODULES_URL) + "bootstrap-sass/assets/fonts/bootstrap/"); ``` and `@import "variables";` whenever you need Glyphicons. You then can safely remove any font references, such as `` from you HTML templates. ### Configure SASS variables through Python functions It is even possible to call Python functions from inside any module. Do this by adding `SASS_PROCESSOR_CUSTOM_FUNCTIONS` to the project's `settings.py`. This shall contain a mapping of SASS function names pointing to a Python function name. Example: ```python SASS_PROCESSOR_CUSTOM_FUNCTIONS = { 'get-color': 'myproject.utils.get_color', } ``` This allows to invoke Python functions out of any `*.scss` file. ```scss $color: get-color(250, 10, 120); ``` Here we pass the parameters '250, 10, 120' into the function `def get_color(red, green, blue)` in Python module `myproject.utils`. Note that this function receives the values as `sass.Number`, hence extract values using `red.value`, etc. If one of these customoized functions returns a value, which is not a string, then convert it either to a Python string or to a value of type `sass.SassNumber`. For other types, refer to their documentation. Such customized functions must accept parameters explicilty, otherwise `sass_processor` does not know how to map them. Variable argument lists therefore can not be used. ## Error reporting Whenever **django-sass-processor** runs in debug mode and fails to compile a SASS/SCSS file, it raises a `sass.CompileError` exception. This shows the location of the error directly on the Django debug console and is very useful during development. This behaviour can be overridden using the settings variable `SASS_PROCESSOR_FAIL_SILENTLY`. If it is set to `True`, instead of raising that exception, the compilation error message is send to the Django logger. ## Using other storage backends for compiled CSS files Under the hood, SASS processor will use any storage configured in your settings as `STATICFILES_STORAGE`. This means you can use anything you normally use for serving static files, e.g. S3. A custom Storage class can be used if your deployment needs to serve generated CSS files from elsewhere, e.g. when your static files storage is not writable at runtime and you nede to re-compile CSS in production. To use a custom storage, configure it in `SASS_PROCESSOR_STORAGE`. You can also configure a dictionary with options that will be passed to the storage class as keyword arguments in `SASS_PROCESSOR_STORAGE_OPTIONS` (e.g. if you want to use `FileSystemStorage`, but with a different `location` or `base_url`: ```python SASS_PROCESSOR_STORAGE = 'django.core.files.storage.FileSystemStorage' SASS_PROCESSOR_STORAGE_OPTIONS = { 'location': '/srv/media/generated', 'base_url': 'https://media.myapp.example.com/generated' } ``` ### Amazon's S3 Storage Using the S3 storage backend from [django-storages](https://django-storages.readthedocs.io/en/latest/) with its regular configuration (if you do not otherwise use it for service static files): ```python SASS_PROCESSOR_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' ``` ## Heroku If you are deploying to [Heroku](https://www.heroku.com/), use the [heroku-buildpack-django-sass](https://elements.heroku.com/buildpacks/drpancake/heroku-buildpack-django-sass) buildpack to automatically compile scss for you. ## Development To run the tests locally, clone the repository, and, in your local copy, create a new virtualenv. these commands: ```shell python -m pip install --upgrade pip pip install Django pip install -r tests/requirements.txt python -m pytest tests ``` ## Third Party Documentation Brian wrote a nice artice on [how to easily use SASS/SCSS with Django](https://engineertodeveloper.com/how-to-easily-use-sass-scss-with-django/). django-sass-processor-1.2.2/manage.py000066400000000000000000000000641432556561500175600ustar00rootroot00000000000000# This file only exists to make pytest-django work. django-sass-processor-1.2.2/pytest.ini000066400000000000000000000001071432556561500200050ustar00rootroot00000000000000[pytest] DJANGO_SETTINGS_MODULE = tests.settings addopts = --tb native django-sass-processor-1.2.2/sass_processor/000077500000000000000000000000001432556561500210265ustar00rootroot00000000000000django-sass-processor-1.2.2/sass_processor/__init__.py000066400000000000000000000014611432556561500231410ustar00rootroot00000000000000""" See PEP 386 (https://www.python.org/dev/peps/pep-0386/) Release logic: 1. Remove ".devX" from __version__ (below) 2. Remove ".devX" latest version in README.md / Changelog 3. git add sass_processor/__init__.py 4. git commit -m 'Bump to ' 5. git push 6. (assure that all tests pass) 7. git tag 8. git push --tags 9. python setup.py sdist upload 10. bump the version, append ".dev0" to __version__ 11. Add a new heading to README.md / Changelog, named ".dev" 12. git add sass_processor/__init__.py README.md 12. git commit -m 'Start with ' 13. git push """ __version__ = '1.2.2' import django if django.VERSION < (3, 2): # https://docs.djangoproject.com/en/dev/releases/3.2/#whats-new-3-2 default_app_config = 'sass_processor.apps.SassProcessorConfig' django-sass-processor-1.2.2/sass_processor/apps.py000066400000000000000000000022311432556561500223410ustar00rootroot00000000000000import re import os from django.apps import apps, AppConfig from django.conf import settings from django.contrib.staticfiles.finders import AppDirectoriesFinder APPS_INCLUDE_DIRS = [] class SassProcessorConfig(AppConfig): name = 'sass_processor' verbose_name = "Sass Processor" auto_include = getattr(settings, 'SASS_PROCESSOR_AUTO_INCLUDE', True) _pattern = re.compile(getattr(settings, 'SASS_PROCESSOR_INCLUDE_FILE_PATTERN', r'^_.+\.(scss|sass)$')) def ready(self): if self.auto_include: app_configs = apps.get_app_configs() for app_config in app_configs: static_dir = os.path.join(app_config.path, AppDirectoriesFinder.source_dir) if os.path.isdir(static_dir): self.traverse_tree(static_dir) @classmethod def traverse_tree(cls, static_dir): """traverse the static folders an look for at least one file ending in .scss/.sass""" for root, dirs, files in os.walk(static_dir): for filename in files: if cls._pattern.match(filename): APPS_INCLUDE_DIRS.append(static_dir) return django-sass-processor-1.2.2/sass_processor/finders.py000066400000000000000000000012371432556561500230350ustar00rootroot00000000000000from django.contrib.staticfiles.finders import BaseStorageFinder from .storage import SassFileStorage class CssFinder(BaseStorageFinder): """ Find static *.css files compiled on the fly using templatetag `{% sass_src "" %}` and stored in configured storage. """ storage = SassFileStorage() def list(self, ignore_patterns): """ Do not list the contents of the configured storages, since this has already been done by other finders. This prevents the warning ``Found another file with the destination path ...``, while issuing ``./manage.py collectstatic``. """ if False: yield django-sass-processor-1.2.2/sass_processor/jinja2/000077500000000000000000000000001432556561500222035ustar00rootroot00000000000000django-sass-processor-1.2.2/sass_processor/jinja2/__init__.py000066400000000000000000000000001432556561500243020ustar00rootroot00000000000000django-sass-processor-1.2.2/sass_processor/jinja2/ext.py000066400000000000000000000012701432556561500233550ustar00rootroot00000000000000from jinja2 import nodes from jinja2.ext import Extension from sass_processor.processor import SassProcessor class SassSrc(Extension): tags = set(['sass_src']) def parse(self, parser): lineno = next(parser.stream).lineno path = parser.parse_expression() call = self.call_method( '_sass_src_support', [ path, nodes.Const(parser.filename) ] ) return nodes.Output( [call], lineno=lineno ) def _sass_src_support(self, path, source_file): sass_processor = SassProcessor(source_file) return SassProcessor.handle_simple(sass_processor(path)) django-sass-processor-1.2.2/sass_processor/management/000077500000000000000000000000001432556561500231425ustar00rootroot00000000000000django-sass-processor-1.2.2/sass_processor/management/__init__.py000066400000000000000000000000001432556561500252410ustar00rootroot00000000000000django-sass-processor-1.2.2/sass_processor/management/commands/000077500000000000000000000000001432556561500247435ustar00rootroot00000000000000django-sass-processor-1.2.2/sass_processor/management/commands/__init__.py000066400000000000000000000000001432556561500270420ustar00rootroot00000000000000django-sass-processor-1.2.2/sass_processor/management/commands/compilescss.py000066400000000000000000000335621432556561500276520ustar00rootroot00000000000000import os import ast from importlib import import_module import sass from compressor.exceptions import TemplateDoesNotExist, TemplateSyntaxError from django.apps import apps from django.conf import settings from django.core.files.base import ContentFile from django.core.management.base import BaseCommand, CommandError from django.template import engines from django.template.base import Origin from django.template.loader import \ get_template # in order to preload template locations from django.utils.encoding import force_bytes from django.utils.translation import gettext_lazy as _ from sass_processor.apps import APPS_INCLUDE_DIRS from sass_processor.processor import SassProcessor from sass_processor.storage import SassFileStorage, find_file from sass_processor.templatetags.sass_tags import SassSrcNode from sass_processor.utils import get_custom_functions __all__ = ['get_template', 'Command'] class FuncCallVisitor(ast.NodeVisitor): def __init__(self, func_name): self.func_name = func_name self.sass_files = [] def visit_Call(self, node): try: if node.func.id == self.func_name: arg0 = dict((a, b) for a, b in ast.iter_fields(node))['args'][0] self.sass_files.append(getattr(arg0, arg0._fields[0])) except AttributeError: pass self.generic_visit(node) class Command(BaseCommand): help = "Compile SASS/SCSS into CSS outside of the request/response cycle" storage = SassFileStorage() def __init__(self): self.parser = None self.template_exts = getattr(settings, 'SASS_TEMPLATE_EXTS', ['.html']) self.sass_output_style = getattr(settings, 'SASS_OUTPUT_STYLE', 'nested' if settings.DEBUG else 'compressed') self.use_storage = False super().__init__() def add_arguments(self, parser): parser.add_argument( '--delete-files', action='store_true', dest='delete_files', default=False, help=_("Delete generated `*.css` files instead of creating them.") ) parser.add_argument( '--use-storage', action='store_true', dest='use_storage', default=False, help=_("Store resulting .css in configured storage. " "Default: store each css side-by-side with .scss.") ) parser.add_argument( '--engine', dest='engine', default='django', help=_("Set templating engine used (django, jinja2). Default: django.") ) parser.add_argument( '--sass-precision', dest='sass_precision', type=int, help=_( "Set the precision for numeric computations in the SASS processor. Default: settings.SASS_PRECISION.") ) def get_loaders(self): template_source_loaders = [] for e in engines.all(): if hasattr(e, 'engine'): template_source_loaders.extend( e.engine.get_template_loaders( e.engine.loaders ) ) loaders = [] # If template loader is CachedTemplateLoader, return the loaders # that it wraps around. So if we have # TEMPLATE_LOADERS = ( # ('django.template.loaders.cached.Loader', ( # 'django.template.loaders.filesystem.Loader', # 'django.template.loaders.app_directories.Loader', # )), # ) # The loaders will return django.template.loaders.filesystem.Loader # and django.template.loaders.app_directories.Loader # The cached Loader and similar ones include a 'loaders' attribute # so we look for that. for loader in template_source_loaders: if hasattr(loader, 'loaders'): loaders.extend(loader.loaders) else: loaders.append(loader) return loaders def get_parser(self, engine): if engine == 'jinja2': from compressor.offline.jinja2 import Jinja2Parser env = settings.COMPRESS_JINJA2_GET_ENVIRONMENT() parser = Jinja2Parser(charset='utf-8', env=env) elif engine == 'django': from compressor.offline.django import DjangoParser parser = DjangoParser(charset='utf-8') else: raise CommandError( "Invalid templating engine '{engine}' specified.".format( engine=engine ) ) return parser def handle(self, *args, **options): self.verbosity = int(options['verbosity']) self.delete_files = options['delete_files'] self.use_storage = options['use_storage'] engines = [e.strip() for e in options.get('engines', [])] or ['django'] for engine in engines: self.parser = self.get_parser(engine) try: self.sass_precision = int(options['sass_precision'] or settings.SASS_PRECISION) except (AttributeError, TypeError, ValueError): self.sass_precision = None self.processed_files = [] # find all Python files making up this project; They might invoke `sass_processor` for py_source in self.find_sources(): if self.verbosity > 1: self.stdout.write("Parsing file: {}".format(py_source)) elif self.verbosity == 1: self.stdout.write(".", ending="") try: self.parse_source(py_source) except (SyntaxError, IndentationError) as exc: msg = "Syntax error encountered processing {0}: {1}\nAborting compilation." self.stderr.write(msg.format(py_source, exc)) raise # find all Django/Jinja2 templates making up this project; They might invoke `sass_src` templates = self.find_templates() for template_name in templates: self.parse_template(template_name) if self.verbosity > 0: self.stdout.write(".", ending="") # summarize what has been done if self.verbosity > 0: self.stdout.write("") if self.delete_files: msg = "Successfully deleted {0} previously generated `*.css` files." self.stdout.write(msg.format(len(self.processed_files))) else: msg = "Successfully compiled {0} referred SASS/SCSS files." self.stdout.write(msg.format(len(self.processed_files))) def find_sources(self): """ Look for Python sources available for the current configuration. """ app_config = apps.get_app_config('sass_processor') if app_config.auto_include: app_configs = apps.get_app_configs() for app_config in app_configs: ignore_dirs = [] for root, dirs, files in os.walk(app_config.path): if [True for idir in ignore_dirs if root.startswith(idir)]: continue if '__init__.py' not in files: ignore_dirs.append(root) continue for filename in files: basename, ext = os.path.splitext(filename) if ext != '.py': continue yield os.path.abspath(os.path.join(root, filename)) def parse_source(self, filename): """ Extract the statements from the given file, look for function calls `sass_processor(scss_file)` and compile the filename into CSS. """ callvisitor = FuncCallVisitor('sass_processor') tree = ast.parse(open(filename, 'rb').read()) callvisitor.visit(tree) for sass_fileurl in callvisitor.sass_files: sass_filename = find_file(sass_fileurl) if not sass_filename or sass_filename in self.processed_files: continue if self.delete_files: self.delete_file(sass_filename, sass_fileurl) else: self.compile_sass(sass_filename, sass_fileurl) def find_templates(self): """ Look for templates and extract the nodes containing the SASS file. """ paths = set() for loader in self.get_loaders(): try: module = import_module(loader.__module__) get_template_sources = getattr( module, 'get_template_sources', loader.get_template_sources) template_sources = get_template_sources('') paths.update([t.name if isinstance(t, Origin) else t for t in template_sources]) except (ImportError, AttributeError): pass if not paths: raise CommandError( "No template paths found. None of the configured template loaders provided template paths") templates = set() for path in paths: for root, _, files in os.walk(str(path)): templates.update(os.path.join(root, name) for name in files if not name.startswith('.') and any(name.endswith(ext) for ext in self.template_exts)) if not templates: raise CommandError( "No templates found. Make sure your TEMPLATE_LOADERS and TEMPLATE_DIRS settings are correct.") return templates def parse_template(self, template_name): try: template = self.parser.parse(template_name) except IOError: # unreadable file -> ignore if self.verbosity > 0: self.stderr.write("\nUnreadable template at: {}".format(template_name)) return except TemplateSyntaxError as e: # broken template -> ignore if self.verbosity > 0: self.stderr.write("\nInvalid template {}: {}".format(template_name, e)) return except TemplateDoesNotExist: # non existent template -> ignore if self.verbosity > 0: self.stderr.write("\nNon-existent template at: {}".format(template_name)) return except UnicodeDecodeError: if self.verbosity > 0: self.stderr.write( "\nUnicodeDecodeError while trying to read template {}".format(template_name)) try: nodes = list(self.walk_nodes(template, original=template)) except Exception as e: # Could be an error in some base template if self.verbosity > 0: self.stderr.write("\nError parsing template {}: {}".format(template_name, e)) else: for node in nodes: sass_filename = find_file(node.path) if not sass_filename or sass_filename in self.processed_files: continue if self.delete_files: self.delete_file(sass_filename, node.path) else: self.compile_sass(sass_filename, node.path) def compile_sass(self, sass_filename, sass_fileurl): """ Compile the given SASS file into CSS """ compile_kwargs = { 'filename': sass_filename, 'include_paths': SassProcessor.include_paths + APPS_INCLUDE_DIRS, 'custom_functions': get_custom_functions(), } if self.sass_precision: compile_kwargs['precision'] = self.sass_precision if self.sass_output_style: compile_kwargs['output_style'] = self.sass_output_style content = sass.compile(**compile_kwargs) self.save_to_destination(content, sass_filename, sass_fileurl) self.processed_files.append(sass_filename) if self.verbosity > 1: self.stdout.write("Compiled SASS/SCSS file: '{0}'\n".format(sass_filename)) def delete_file(self, sass_filename, sass_fileurl): """ Delete a *.css file, but only if it has been generated through a SASS/SCSS file. """ if self.use_storage: destpath = os.path.splitext(sass_fileurl)[0] + '.css' if self.storage.exists(destpath): self.storage.delete(destpath) else: return else: destpath = os.path.splitext(sass_filename)[0] + '.css' if os.path.isfile(destpath): os.remove(destpath) else: return self.processed_files.append(sass_filename) if self.verbosity > 1: self.stdout.write("Deleted '{0}'\n".format(destpath)) def save_to_destination(self, content, sass_filename, sass_fileurl): if self.use_storage: basename, _ = os.path.splitext(sass_fileurl) destpath = basename + '.css' if self.storage.exists(destpath): self.storage.delete(destpath) self.storage.save(destpath, ContentFile(content)) else: basename, _ = os.path.splitext(sass_filename) destpath = basename + '.css' with open(destpath, 'wb') as fh: fh.write(force_bytes(content)) def walk_nodes(self, node, original): """ Iterate over the nodes recursively yielding the templatetag 'sass_src' """ try: # try with django-compressor<2.1 nodelist = self.parser.get_nodelist(node, original=original) except TypeError: nodelist = self.parser.get_nodelist(node, original=original, context=None) for node in nodelist: if isinstance(node, SassSrcNode): if node.is_sass: yield node else: for node in self.walk_nodes(node, original=original): yield node django-sass-processor-1.2.2/sass_processor/processor.py000066400000000000000000000136361432556561500234300ustar00rootroot00000000000000import os import json import logging import subprocess from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.core.files.base import ContentFile from django.template import Context from django.utils.encoding import force_bytes from sass_processor.utils import get_custom_functions from .storage import SassFileStorage, find_file from .apps import APPS_INCLUDE_DIRS try: import sass except ImportError: sass = None logger = logging.getLogger('sass-processor') class SassProcessor: source_storage = SassFileStorage() include_paths = [str(ip) for ip in getattr(settings, 'SASS_PROCESSOR_INCLUDE_DIRS', [])] try: sass_precision = int(settings.SASS_PRECISION) except (AttributeError, TypeError, ValueError): sass_precision = None sass_output_style = getattr( settings, 'SASS_OUTPUT_STYLE', 'nested' if settings.DEBUG else 'compressed') processor_enabled = getattr(settings, 'SASS_PROCESSOR_ENABLED', settings.DEBUG) fail_silently = getattr(settings, 'SASS_PROCESSOR_FAIL_SILENTLY', not settings.DEBUG) sass_extensions = ('.scss', '.sass') node_npx_path = getattr(settings, 'NODE_NPX_PATH', 'npx') def __init__(self, path=None): self._path = path nmd = [d[1] for d in getattr(settings, 'STATICFILES_DIRS', []) if isinstance(d, (list, tuple)) and d[0] == 'node_modules'] self.node_modules_dir = str(nmd[0]) if len(nmd) else None def __call__(self, path): basename, ext = os.path.splitext(path) filename = find_file(path) if filename is None: raise FileNotFoundError("Unable to locate file {path}".format(path=path)) if ext not in self.sass_extensions: # return the given path, since it ends neither in `.scss` nor in `.sass` return path # compare timestamp of sourcemap file with all its dependencies, and check if we must recompile css_filename = basename + '.css' if not self.processor_enabled: return css_filename sourcemap_filename = css_filename + '.map' base = os.path.dirname(filename) if self.source_storage.exists(css_filename) and self.is_latest(sourcemap_filename, base): return css_filename # with offline compilation, raise an error, if css file could not be found. if sass is None: msg = "Offline compiled file `{}` is missing and libsass has not been installed." raise ImproperlyConfigured(msg.format(css_filename)) # otherwise compile the SASS/SCSS file into .css and store it filename_map = filename.replace(ext, '.css.map') compile_kwargs = { 'filename': filename, 'source_map_filename': filename_map, 'include_paths': self.include_paths + APPS_INCLUDE_DIRS, 'custom_functions': get_custom_functions(), } if self.sass_precision: compile_kwargs['precision'] = self.sass_precision if self.sass_output_style: compile_kwargs['output_style'] = self.sass_output_style try: content, sourcemap = (force_bytes(output) for output in sass.compile(**compile_kwargs)) except sass.CompileError as exc: if self.fail_silently: content, sourcemap = force_bytes(exc), None logger.error(exc) else: raise exc # autoprefix CSS files using postcss in external JavaScript process if self.node_npx_path and os.path.isdir(self.node_modules_dir or ''): os.environ['NODE_PATH'] = self.node_modules_dir try: options = [self.node_npx_path, 'postcss', '--use', 'autoprefixer'] if not settings.DEBUG: options.append('--no-map') proc = subprocess.Popen(options, stdin=subprocess.PIPE, stdout=subprocess.PIPE) proc.stdin.write(content) proc.stdin.close() autoprefixed_content = proc.stdout.read() proc.wait() except (FileNotFoundError, BrokenPipeError) as exc: logger.warning("Unable to postcss {}. Reason: {}".format(filename, exc)) else: if len(autoprefixed_content) >= len(content): content = autoprefixed_content if self.source_storage.exists(css_filename): self.source_storage.delete(css_filename) self.source_storage.save(css_filename, ContentFile(content)) if self.source_storage.exists(sourcemap_filename): self.source_storage.delete(sourcemap_filename) if sourcemap: self.source_storage.save(sourcemap_filename, ContentFile(sourcemap)) return css_filename def resolve_path(self, context=None): if context is None: context = Context() return self._path.resolve(context) def is_sass(self): _, ext = os.path.splitext(self.resolve_path()) return ext in self.sass_extensions def is_latest(self, sourcemap_file, base): if not self.source_storage.exists(sourcemap_file): return False sourcemap_mtime = self.source_storage.get_modified_time(sourcemap_file).timestamp() with self.source_storage.open(sourcemap_file, 'r') as fp: sourcemap = json.load(fp) for srcfilename in sourcemap.get('sources'): srcfilename = os.path.join(base, srcfilename) if not os.path.isfile(srcfilename) or os.stat(srcfilename).st_mtime > sourcemap_mtime: # at least one of the source is younger that the sourcemap referring it return False return True @classmethod def handle_simple(cls, path): return cls.source_storage.url(path) _sass_processor = SassProcessor() def sass_processor(filename): path = _sass_processor(filename) return SassProcessor.handle_simple(path) django-sass-processor-1.2.2/sass_processor/storage.py000066400000000000000000000017201432556561500230440ustar00rootroot00000000000000from django.conf import settings from django.contrib.staticfiles.finders import get_finders from django.core.files.storage import FileSystemStorage, get_storage_class from django.utils.functional import LazyObject class SassFileStorage(LazyObject): def _setup(self): storage_path = getattr(settings, 'SASS_PROCESSOR_STORAGE', settings.STATICFILES_STORAGE) storage_options = getattr(settings, 'SASS_PROCESSOR_STORAGE_OPTIONS', {}) storage_class = get_storage_class(storage_path) if storage_path == settings.STATICFILES_STORAGE and issubclass(storage_class, FileSystemStorage): storage_options['location'] = getattr(settings, 'SASS_PROCESSOR_ROOT', settings.STATIC_ROOT) storage_options['base_url'] = settings.STATIC_URL self._wrapped = storage_class(**storage_options) def find_file(path): for finder in get_finders(): result = finder.find(path) if result: return result django-sass-processor-1.2.2/sass_processor/templatetags/000077500000000000000000000000001432556561500235205ustar00rootroot00000000000000django-sass-processor-1.2.2/sass_processor/templatetags/__init__.py000066400000000000000000000000001432556561500256170ustar00rootroot00000000000000django-sass-processor-1.2.2/sass_processor/templatetags/sass_tags.py000066400000000000000000000027601432556561500260660ustar00rootroot00000000000000from django.template import Library from django.template.base import Node, TemplateSyntaxError from sass_processor.processor import SassProcessor try: FileNotFoundError except NameError: FileNotFoundError = IOError register = Library() class SassSrcNode(Node): def __init__(self, path): self.sass_processor = SassProcessor(path) @classmethod def handle_token(cls, parser, token): bits = token.split_contents() if len(bits) != 2: raise TemplateSyntaxError("'{0}' takes a URL to a CSS file as its only argument".format(*bits)) path = parser.compile_filter(bits[1]) return cls(path) @property def path(self): return self.sass_processor.resolve_path() @property def is_sass(self): return self.sass_processor.is_sass() def render(self, context): try: path = self.sass_processor(self.sass_processor.resolve_path(context)) except AttributeError as e: msg = "No sass/scss file specified while rendering tag 'sass_src' in template {} ({})" raise TemplateSyntaxError(msg.format(context.template_name, e)) except FileNotFoundError as e: msg = str(e) + " while rendering tag 'sass_src' in template {}" raise TemplateSyntaxError(msg.format(context.template_name)) return SassProcessor.handle_simple(path) @register.tag(name='sass_src') def render_sass_src(parser, token): return SassSrcNode.handle_token(parser, token) django-sass-processor-1.2.2/sass_processor/types.py000066400000000000000000000005121432556561500225420ustar00rootroot00000000000000from decimal import Decimal from sass import SassNumber as _SassNumber, SassColor, SassList, SassMap __all__ = ['SassNumber', 'SassColor', 'SassList', 'SassMap'] def SassNumber(value): if isinstance(value, (int, float, Decimal)): return str(_SassNumber(value, type(value).__name__.encode()).value) return value django-sass-processor-1.2.2/sass_processor/utils.py000066400000000000000000000025051432556561500225420ustar00rootroot00000000000000import inspect from django.conf import settings from django.template import TemplateSyntaxError from django.utils.module_loading import import_string try: import sass except ImportError: sass = None def get_custom_functions(): """ Return a dict of function names, to be used from inside SASS """ def get_setting(*args): try: return getattr(settings, args[0]) except AttributeError as e: raise TemplateSyntaxError(str(e)) if hasattr(get_custom_functions, '_custom_functions'): return get_custom_functions._custom_functions get_custom_functions._custom_functions = {sass.SassFunction('get-setting', ('key',), get_setting)} for name, func in getattr(settings, 'SASS_PROCESSOR_CUSTOM_FUNCTIONS', {}).items(): try: if isinstance(func, str): func = import_string(func) except Exception as e: raise TemplateSyntaxError(str(e)) else: if not inspect.isfunction(func): raise TemplateSyntaxError("{} is not a Python function".format(func)) func_args = inspect.getfullargspec(func).args sass_func = sass.SassFunction(name, func_args, func) get_custom_functions._custom_functions.add(sass_func) return get_custom_functions._custom_functions django-sass-processor-1.2.2/setup.cfg000066400000000000000000000022271432556561500176020ustar00rootroot00000000000000[metadata] name = django-sass-processor version = attr: sass_processor.__version__ description = SASS processor to compile SCSS files into *.css, while rendering, or offline. long_description = file: README.md long_description_content_type = text/markdown author = Jacob Rief author_email = jacob.rief@gmail.com url = https://github.com/jrief/django-sass-processor license = MIT license_file = LICENSE-MIT keywords = django, sass classifiers = Development Status :: 5 - Production/Stable Environment :: Web Environment Framework :: Django Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent Programming Language :: Python Topic :: Internet :: WWW/HTTP :: Dynamic Content Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Framework :: Django :: 2.2 Framework :: Django :: 3.2 Framework :: Django :: 4.0 [options] packages = find: zip_safe = False [options.packages.find] exclude = tests [options.extras_require] management-command = django-compressor>=2.4 django-sass-processor-1.2.2/setup.py000066400000000000000000000000741432556561500174710ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import setup setup() django-sass-processor-1.2.2/tests/000077500000000000000000000000001432556561500171205ustar00rootroot00000000000000django-sass-processor-1.2.2/tests/__init__.py000066400000000000000000000004721432556561500212340ustar00rootroot00000000000000from sass_processor.types import SassNumber, SassColor def get_width(): return SassNumber(5) def get_margins(top, right, bottom, left): return "{}px {}px {}px {}px".format(top.value, right.value, bottom.value, left.value) def get_plain_color(r, g, b): return SassColor(r.value, g.value, b.value, 1) django-sass-processor-1.2.2/tests/jinja2.py000066400000000000000000000004131432556561500206450ustar00rootroot00000000000000from jinja2 import Environment def environment(**kwargs): extensions = [] if 'extensions' not in kwargs else kwargs['extensions'] extensions.append('sass_processor.jinja2.ext.SassSrc') kwargs['extensions'] = extensions return Environment(**kwargs) django-sass-processor-1.2.2/tests/jinja2/000077500000000000000000000000001432556561500202755ustar00rootroot00000000000000django-sass-processor-1.2.2/tests/jinja2/tests/000077500000000000000000000000001432556561500214375ustar00rootroot00000000000000django-sass-processor-1.2.2/tests/jinja2/tests/jinja2.html000066400000000000000000000000451432556561500235010ustar00rootroot00000000000000{% sass_src 'tests/css/main.scss' %} django-sass-processor-1.2.2/tests/jinja2/tests/jinja2_variable.html000066400000000000000000000000771432556561500253530ustar00rootroot00000000000000{% set source = 'tests/css/main.scss' %} {% sass_src source %} django-sass-processor-1.2.2/tests/requirements.txt000066400000000000000000000000661432556561500224060ustar00rootroot00000000000000libsass pytest pytest-django jinja2 django-compressor django-sass-processor-1.2.2/tests/settings.py000066400000000000000000000043341432556561500213360ustar00rootroot00000000000000import os from tests.jinja2 import environment SITE_ID = 1 DATABASE_ENGINE = 'sqlite3' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:', } } INSTALLED_APPS = [ 'django.contrib.contenttypes', 'django.contrib.sites', 'django.contrib.auth', 'django.contrib.admin', 'django.contrib.staticfiles', 'sass_processor', 'tests', ] TEMPLATES = [ { 'NAME': 'django', 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.contrib.auth.context_processors.auth', 'django.template.context_processors.debug', 'django.template.context_processors.i18n', 'django.template.context_processors.media', 'django.template.context_processors.static', 'django.template.context_processors.tz', 'django.contrib.messages.context_processors.messages', ], }, }, { 'NAME': 'jinja2', 'BACKEND': 'django.template.backends.jinja2.Jinja2', 'APP_DIRS': True, 'OPTIONS': { 'environment': 'tests.jinja2.environment' }, } ] COMPRESS_JINJA2_GET_ENVIRONMENT = environment MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', #'django.contrib.auth.middleware.AuthenticationMiddleware', ) USE_TZ = True SECRET_KEY = 'secret' STATIC_URL = '/static/' PROJECT_ROOT = os.path.abspath(os.path.join(__file__, os.path.pardir)) STATICFILES_FINDERS = [ 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'sass_processor.finders.CssFinder', ] STATICFILES_DIRS = [ os.path.join(PROJECT_ROOT, 'static'), ] SASS_PROCESSOR_ENABLED = True SASS_PROCESSOR_CUSTOM_FUNCTIONS = { 'get-width': 'tests.get_width', 'get-margins': 'tests.get_margins', 'get-plain-color': 'tests.get_plain_color', } SASS_BLUE_COLOR = '#0000ff' STATIC_ROOT = os.path.join(os.path.dirname(__file__), "tmpstatic") django-sass-processor-1.2.2/tests/static/000077500000000000000000000000001432556561500204075ustar00rootroot00000000000000django-sass-processor-1.2.2/tests/static/tests/000077500000000000000000000000001432556561500215515ustar00rootroot00000000000000django-sass-processor-1.2.2/tests/static/tests/css/000077500000000000000000000000001432556561500223415ustar00rootroot00000000000000django-sass-processor-1.2.2/tests/static/tests/css/_redbox.scss000066400000000000000000000001101432556561500246500ustar00rootroot00000000000000.redbox { background-color: #ff0000; &:hover { color: #000000; } } django-sass-processor-1.2.2/tests/static/tests/css/bluebox.scss000066400000000000000000000002121432556561500246710ustar00rootroot00000000000000.bluebox { background-color: get-setting(SASS_BLUE_COLOR); margin: get-margins(10, 5, 20, 15); color: get-plain-color(250, 10, 120); } django-sass-processor-1.2.2/tests/static/tests/css/main.scss000066400000000000000000000000761432556561500241650ustar00rootroot00000000000000#main p { color: #00ff00; width: 97%; @import "redbox"; } django-sass-processor-1.2.2/tests/templates/000077500000000000000000000000001432556561500211165ustar00rootroot00000000000000django-sass-processor-1.2.2/tests/templates/tests/000077500000000000000000000000001432556561500222605ustar00rootroot00000000000000django-sass-processor-1.2.2/tests/templates/tests/django.html000066400000000000000000000000711432556561500244060ustar00rootroot00000000000000{% load sass_tags %}{% sass_src 'tests/css/main.scss' %} django-sass-processor-1.2.2/tests/test_sass_processor.py000066400000000000000000000115421432556561500236040ustar00rootroot00000000000000import calendar import os import shutil from datetime import datetime from django.conf import settings from django.core.management import call_command from django.template.loader import get_template from django.test import TestCase, override_settings class SassProcessorTest(TestCase): def setUp(self): super(SassProcessorTest, self).setUp() try: os.mkdir(settings.STATIC_ROOT) except OSError: pass def tearDown(self): shutil.rmtree(settings.STATIC_ROOT) def assert_sass_src_engine(self, template_name, engine): template = get_template( template_name=template_name, using=engine ) # Strip the line breaks. template_content = template.render({}).strip() self.assertEqual('/static/tests/css/main.css', template_content) css_file = os.path.join(settings.STATIC_ROOT, 'tests/css/main.css') self.assertTrue(os.path.exists(css_file)) with open(css_file, 'r') as f: output = f.read() expected = "#main p{color:#00ff00;width:97%}#main p .redbox{background-color:#ff0000}#main p .redbox:hover{color:#000000}\n\n/*# sourceMappingURL=main.css.map */" self.assertEqual(expected, output) # check if compilation is skipped file for a second invocation of `sass_src` timestamp = os.path.getmtime(css_file) template.render({}) self.assertEqual(timestamp, os.path.getmtime(css_file)) # removing `main.css.map` should trigger a recompilation os.remove(css_file + '.map') template.render({}) self.assertTrue(os.path.exists(css_file + '.map')) # if `main.scss` is newer than `main.css`, recompile everything longago = calendar.timegm(datetime(2017, 1, 1).timetuple()) os.utime(css_file, (longago, longago)) template.render({}) self.assertGreater(timestamp, os.path.getmtime(css_file)) def test_sass_src_django(self): self.assert_sass_src_engine( template_name='tests/django.html', engine='django' ) def test_sass_src_jinja2(self): self.assert_sass_src_engine( template_name='tests/jinja2.html', engine='jinja2' ) def test_sass_src_jinja2_variable(self): self.assert_sass_src_engine( template_name='tests/jinja2_variable.html', engine='jinja2' ) def test_sass_processor(self): from sass_processor.processor import sass_processor css_file = sass_processor('tests/css/bluebox.scss') self.assertEqual('/static/tests/css/bluebox.css', css_file) css_file = os.path.join(settings.STATIC_ROOT, 'tests/css/bluebox.css') self.assertTrue(os.path.exists(css_file)) with open(css_file, 'r') as f: output = f.read() expected = '.bluebox{background-color:#0000ff;margin:10.0px 5.0px 20.0px 15.0px;color:#fa0a78}\n\n/*# sourceMappingURL=bluebox.css.map */' self.assertEqual(expected, output) def assert_management_command(self, **kwargs): call_command( 'compilescss', **kwargs ) if kwargs.get('use_storage', False): css_file = os.path.join(settings.STATIC_ROOT, 'tests/css/main.css') else: css_file = os.path.join(settings.PROJECT_ROOT, 'static/tests/css/main.css') with open(css_file, 'r') as f: output = f.read() expected = '#main p{color:#00ff00;width:97%}#main p .redbox{background-color:#ff0000}#main p .redbox:hover{color:#000000}\n' self.assertEqual(expected, output) self.assertFalse(os.path.exists(css_file + '.map')) if not kwargs.get('use_storage', False): call_command('compilescss', delete_files=True) self.assertFalse(os.path.exists(css_file)) @override_settings(DEBUG=False) def test_management_command_django(self): self.assert_management_command( engine='django' ) @override_settings(DEBUG=False) def test_management_command_jinja2(self): self.assert_management_command( engine='jinja2' ) @override_settings(DEBUG=False) def test_management_command_multiple(self): self.assert_management_command( engine=['jinja2', 'django'] ) @override_settings(DEBUG=False) def test_use_storage_django(self): self.assert_management_command( engine='django', use_storage=True ) @override_settings(DEBUG=False) def test_use_storage_jinja2(self): self.assert_management_command( engine='jinja2', use_storage=True ) @override_settings(DEBUG=False) def test_use_storage_multiple(self): self.assert_management_command( engine=['jinja2', 'django'], use_storage=True )