pax_global_header00006660000000000000000000000064141102477740014521gustar00rootroot0000000000000052 comment=16fbb498ae4cd9cbcc00a07be3e27d4c9080c161 django-sass-1.1.0/000077500000000000000000000000001411024777400137315ustar00rootroot00000000000000django-sass-1.1.0/.editorconfig000066400000000000000000000004521411024777400164070ustar00rootroot00000000000000# EditorConfig is awesome: https://EditorConfig.org root = true [*] charset = utf-8 end_of_line = lf indent_style = space insert_final_newline = true max_line_length = 80 trim_trailing_whitespace = true [*.{html,css,js,json,xml,yaml,yml}] indent_size = 2 [*.{md,ps1,sh,py,rst}] indent_size = 4 django-sass-1.1.0/.gitattributes000066400000000000000000000004211411024777400166210ustar00rootroot00000000000000# Explicitly declare text files that should always be normalized and converted # to unix line endings, to reduce cross-platform development issues. *.py text eol=lf *.html text eol=lf *.js text eol=lf *.css text eol=lf *.json text eol=lf *.md text eol=lf *.rst text eol=lf django-sass-1.1.0/.gitignore000066400000000000000000000127521411024777400157300ustar00rootroot00000000000000# Created by https://www.gitignore.io, modified by CodeRed. ####################################### ### Editors ####################################### ### Emacs ### # -*- mode: gitignore; -*- *~ \#*\# /.emacs.desktop /.emacs.desktop.lock *.elc auto-save-list tramp .\#* # Org-mode .org-id-locations *_archive # flymake-mode *_flymake.* # eshell files /eshell/history /eshell/lastdir # elpa packages /elpa/ # reftex files *.rel # Flycheck flycheck_*.el # server auth directory /server/ # projectiles files .projectile # directory configuration .dir-locals.el # network security /network-security.data ### KomodoEdit ### *.komodoproject .komodotools ### PyCharm ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Sonarlint plugin .idea/sonarlint ### SublimeText ### # Cache files for Sublime Text *.tmlanguage.cache *.tmPreferences.cache *.stTheme.cache # Workspace files are user-specific *.sublime-workspace # Project files should be checked into the repository, unless a significant # proportion of contributors will probably not be using Sublime Text # *.sublime-project # SFTP configuration file sftp-config.json # Package control specific files Package Control.last-run Package Control.ca-list Package Control.ca-bundle Package Control.system-ca-bundle Package Control.cache/ Package Control.ca-certs/ Package Control.merged-ca-bundle Package Control.user-ca-bundle oscrypto-ca-bundle.crt bh_unicode_properties.cache # Sublime-github package stores a github token in this file # https://packagecontrol.io/packages/sublime-github GitHub.sublime-settings ### TextMate ### *.tmproj *.tmproject tmtags ### Vim ### # Swap [._]*.s[a-v][a-z] [._]*.sw[a-p] [._]s[a-rt-v][a-z] [._]ss[a-gi-z] [._]sw[a-p] # Session Session.vim # Temporary .netrwhist # Auto-generated tag files tags # Persistent undo [._]*.un~ ### VisualStudioCode ### .vscode/ ### VisualStudioCode Patch ### # Ignore all local history of files .history ####################################### ### Django/Python Stack ####################################### ### Django ### *.log *.pot *.pyc __pycache__/ local_settings.py db.sqlite3 # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ # in your Git repository. Update and uncomment the following line accordingly. # /staticfiles/ ### Django.Python Stack ### # Byte-compiled / optimized / DLL files *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # 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/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ junit/ # Translations *.mo # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ ### OSX ### # General .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 ####################################### ### Operating Systems ####################################### ### Windows ### # Windows thumbnail cache files Thumbs.db ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk ####################################### ### Custom ####################################### testproject/**/*.css django-sass-1.1.0/LICENSE000066400000000000000000000027731411024777400147470ustar00rootroot00000000000000BSD License Copyright (c) 2019, CodeRed LLC All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-sass-1.1.0/MANIFEST.in000066400000000000000000000001461411024777400154700ustar00rootroot00000000000000include LICENSE *.rst *.txt *.md graft django_sass global-exclude __pycache__ global-exclude *.py[co] django-sass-1.1.0/README.md000066400000000000000000000206551411024777400152200ustar00rootroot00000000000000django-sass =========== The absolute simplest way to use [Sass](https://sass-lang.com/) with Django. Pure Python, minimal dependencies, and no special configuration required. [Source code on GitHub](https://github.com/coderedcorp/django-sass) Status ------ | | | |------------------------|----------------------| | Python Package | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-sass)](https://pypi.org/project/django-sass/) [![PyPI - Django Version](https://img.shields.io/pypi/djversions/django-sass)](https://pypi.org/project/django-sass/) [![PyPI - Wheel](https://img.shields.io/pypi/wheel/django-sass)](https://pypi.org/project/django-sass/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/django-sass)](https://pypi.org/project/django-sass/) [![PyPI](https://img.shields.io/pypi/v/django-sass)](https://pypi.org/project/django-sass/) | | Build | [![Build Status](https://dev.azure.com/coderedcorp/cr-github/_apis/build/status/django-sass?branchName=main)](https://dev.azure.com/coderedcorp/cr-github/_build/latest?definitionId=10&branchName=main) [![Azure DevOps tests (branch)](https://img.shields.io/azure-devops/tests/coderedcorp/cr-github/10/main)](https://dev.azure.com/coderedcorp/cr-github/_build/latest?definitionId=10&branchName=main) [![Azure DevOps coverage (branch)](https://img.shields.io/azure-devops/coverage/coderedcorp/cr-github/10/main)](https://dev.azure.com/coderedcorp/cr-github/_build/latest?definitionId=10&branchName=main) | Installation ------------ 1. Install from pip. ``` pip install django-sass ``` 2. Add to your `INSTALLED_APPS` (you only need to do this in a dev environment, you would not want this in your production settings file, although it adds zero overhead): ```python INSTALLED_APPS = [ ..., 'django_sass', ] ``` 3. Congratulations, you're done 😀 Usage ----- In your app's static files, use Sass as normal. The only difference is that you can **not** traverse upwards using `../` in `@import` statements. For example: ``` app1/ |- static/ |- app1/ |- scss/ |- _colors.scss |- app1.scss app2/ |- static/ |- app2/ |- scss/ |- _colors.scss |- app2.scss ``` In `app2.scss` you could reference app1's and app2's `_colors.scss` import as so: ```scss @import 'app1/scss/colors'; @import 'app2/scss/colors'; // Or since you are in app2, you can reference its colors with a relative path. @import 'colors'; ``` Then to compile `app2.scss` and put it in the `css` directory, run the following management command (the `-g` will build a source map, which is helpful for debugging CSS): ``` python manage.py sass app2/static/app2/scss/app2.scss app2/static/app2/css/app2.css -g ``` Or, you can compile the entire `scss` directory into a corresponding `css` directory. This will traverse all subdirectories as well: ``` python manage.py sass app2/static/app2/scss/ app2/static/app2/css/ ``` In your Django HTML template, reference the CSS file as normal: ```html {% load static %} ``` ✨✨ **Congratulations, you are now a Django + Sass developer!** ✨✨ Now you can commit those CSS files to version control, or run `collectstatic` and deploy them as normal. For an example project layout, see `testproject/` in this repository. Watch Mode ---------- To have `django-sass` watch files and recompile them as they change (useful in development), add the ``--watch`` flag. ``` python manage.py sass app2/static/app2/scss/ app2/static/app2/css/ --watch ``` Example: deploying compressed CSS to production ----------------------------------------------- To compile minified CSS, use the `-t` flag to specify compression level (one of: "expanded", "nested", "compact", "compressed"). The default is "expanded" which is human-readable. ``` python manage.py sass app2/static/app2/scss/ app2/static/app2/css/ -t compressed ``` You may now optionally commit the CSS files to version control if so desired, or omit them, whatever fits your needs better. Then run `collectsatic` as normal. ``` python manage.py collectstatic ``` And now proceed with deploying your files as normal. Limitations ----------- * `@import` statements must reference a path relative to a path in `STATICFILES_FINDERS` (which will usually be an app's `static/` directory or some other directory specified in `STATICFILES_DIRS`). Or they can reference a relative path equal to or below the current file. It does not support traversing up the filesystem (i.e. `../`). Legal imports: ```scss @import 'file-from-currdir'; @import 'subdir/file'; @import 'another-app/file'; ``` Illegal imports: ```scss @import '../file'; ``` * Only supports `-g`, `-p`, and `-t` options similar to `pysassc`. Ideally `django-sass` will be as similar as possible to the `pysassc` command line interface. Feel free to file an issue or make a pull request to improve any of these limitations. 🐱‍💻 Why django-sass? ---------------- Other packages such as [django-libsass](https://github.com/torchbox/django-libsass) and [django-sass-processor](https://github.com/jrief/django-sass-processor), while nice packages, require `django-compressor` which itself depends on several other packages that require compilation to install. Installing `django-compressor` in your production web server requires a LOT of extra bloat including a C compiler. It then will compile the Sass on-the-fly while rendering the HTML templates. This is a wasteful use of CPU on your web server. Instead, `django-sass` lets you compile the Sass locally on your machine *before* deploying, to reduce dependencies and CPU time on your production web server. This helps keep things fast and simple. * If you simply want to use Sass in development without installing a web of unwanted dependencies, then `django-sass` is for you. * If you don't want to deploy any processors or compressors to your production server, then `django-sass` is for you. * If you don't want to change the way you reference and serve static files, then `django-sass` is for you. * And if you want the absolute simplest installation and setup possible for doing Sass, `django-sass` is for you too. django-sass only depends on libsass (which provides pre-built wheels for Windows, Mac, and Linux), and of course Django (any version). Programmatically Compiling Sass ------------------------------- You can also use `django-sass` in Python to programmatically compile the sass. This is useful for build scripts and static site generators. ```python from django_sass import compile_sass # Compile scss and write to output file. compile_sass( inpath="/path/to/file.scss", outpath="/path/to/output.css", output_style="compressed", precision=8, source_map=True ) ``` For more advanced usage, you can specify additional sass search paths outside of your Django project by using the `include_paths` argument. ```python from django_sass import compile_sass, find_static_paths # Get Django's static paths. dirs = find_static_paths() # Add external paths. dirs.append("/external/path/") # Compile scss and write to output file. compile_sass( inpath="/path/to/file.scss", outpath="/path/to/output.css", output_style="compressed", precision=8, source_map=True, include_paths=dirs, ) ``` Contributing ------------ To set up a development environment, first check out this repository, create a venv, then: ``` (myvenv)$ pip install -r requirements-dev.txt ``` Before committing, run static analysis tools: ``` (myvenv)$ black . (myvenv)$ flake8 (myvenv)$ mypy ``` Then run the unit tests: ``` (myvenv)$ pytest ``` Changelog --------- #### 1.1.0 * New: Now compiles `.sass` files as well as `.scss` files. * Fix bug when input path is a file and output path does not exist. #### 1.0.1 * Maintanence release, no functional changes. * Add additional type hints within the codebase. * Tested against Django 3.1 * Formatted code with `black`. #### 1.0.0 * New: You can now use `django_sass` APIs directly in Python. * Added unit tests. * Code quality improvements. #### 0.2.0 * New feature: `-g` option to build a source map (when input is a file, not a directory). #### 0.1.2 * Fix: Write compiled CSS files as UTF-8. * Change: Default `-p` precision from 5 to 8 for better support building Bootstrap CSS. #### 0.1.1 * Fix: Create full file path if not exists when specifying a file output. #### 0.1.0 * Initial release django-sass-1.1.0/azure-pipelines.yml000066400000000000000000000056451411024777400176020ustar00rootroot00000000000000# Add steps that analyze code, save build artifacts, deploy, and more: # https://docs.microsoft.com/azure/devops/pipelines/languages/python # # NOTES: # # Display name of each step should be prefixed with one of the following: # CR-QC: for quality control measures. # CR-BUILD: for build-related tasks. # CR-DEPLOY: for publication or deployment. # [no prefix]: for unrelated CI setup/tooling. # # Use PowerShell Core for any utility scripts so they are re-usable across # Windows, macOS, and Linux. # trigger: - main stages: - stage: Unit_Tests displayName: Unit Tests jobs: - job: pytest displayName: pytest pool: vmImage: 'ubuntu-latest' strategy: matrix: py3.6: PYTHON_VERSION: '3.6' py3.7: PYTHON_VERSION: '3.7' py3.8: PYTHON_VERSION: '3.8' py3.9: PYTHON_VERSION: '3.9' steps: - task: UsePythonVersion@0 displayName: 'Use Python version' inputs: versionSpec: '$(PYTHON_VERSION)' architecture: 'x64' - script: python -m pip install -r requirements-dev.txt displayName: 'CR-QC: Install from local repo' - script: pytest ./testproject/ displayName: 'CR-QC: Run unit tests' - task: PublishTestResults@2 displayName: 'Publish unit test report' condition: succeededOrFailed() inputs: testResultsFiles: '**/test-*.xml' testRunTitle: 'Publish test results for Python $(python.version)' - task: PublishCodeCoverageResults@1 displayName: 'Publish code coverage report' condition: succeededOrFailed() inputs: codeCoverageTool: Cobertura summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage.xml' - stage: Static_Analysis displayName: Static Analysis dependsOn: Unit_Tests condition: succeeded('Unit_Tests') jobs: - job: lint displayName: Linters pool: vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 displayName: 'Use Python version' inputs: versionSpec: '3.9' architecture: 'x64' - script: python -m pip install -r requirements-dev.txt displayName: 'CR-QC: Install from local repo' - script: flake8 . displayName: 'CR-QC: Static analysis (flake8)' - script: black --check . displayName: 'CR-QC: Format check' - script: mypy . displayName: 'CR-QC: Type check (mypy)' - job: codecov displayName: Code Coverage pool: vmImage: 'ubuntu-latest' steps: - task: DownloadPipelineArtifact@2 displayName: 'Download code coverage from current build' inputs: source: 'current' path: '$(Agent.WorkFolder)/current-artifacts' project: '$(System.TeamProjectId)' pipeline: '$(System.DefinitionId)' - pwsh: ./ci/compare-codecov.ps1 -wd $Env:WorkDir displayName: 'CR-QC: Compare code coverage' env: WorkDir: $(Agent.WorkFolder) django-sass-1.1.0/ci/000077500000000000000000000000001411024777400143245ustar00rootroot00000000000000django-sass-1.1.0/ci/compare-codecov.ps1000066400000000000000000000056701411024777400200270ustar00rootroot00000000000000#!/usr/bin/env pwsh <# .SYNOPSIS Compares code coverage percent of local coverage.xml file to main branch (Azure Pipeline API). .PARAMETER wd The working directory in which to search for current coverage.xml. .PARAMETER org Name of the Azure DevOps organization where the pipeline is hosted. .PARAMETER project Name of the Azure DevOps project to which the pipeline belongs. .PARAMETER pipeline_name Name of the desired pipeline within the project. This is to support projects with multiple pipelines. #> # ---- SETUP ------------------------------------------------------------------- param( [string] $wd = (Get-Item (Split-Path $PSCommandPath -Parent)).Parent, [string] $org = "coderedcorp", [string] $project = "cr-github", [string] $pipeline_name = "django-sass" ) # Hide "UI" and progress bars. $ProgressPreference = "SilentlyContinue" # API setup. $ApiBase = "https://dev.azure.com/$org/$project" # ---- GET CODE COVERAGE FROM RECENT BUILD ------------------------------------- # Get list of all recent builds. $mainBuildJson = ( Invoke-WebRequest "$ApiBase/_apis/build/builds?branchName=refs/heads/main&api-version=5.1" ).Content | ConvertFrom-Json # Get the latest matching build ID from the list of builds. foreach ($build in $mainBuildJson.value) { if ($build.definition.name -eq $pipeline_name) { $mainLatestId = $build.id break } } # Retrieve code coverage for this build ID. $mainCoverageJson = ( Invoke-WebRequest "$ApiBase/_apis/test/codecoverage?buildId=$mainLatestId&api-version=5.1-preview.1" ).Content | ConvertFrom-Json foreach ($cov in $mainCoverageJson.coverageData.coverageStats) { if ($cov.label -eq "Lines") { $mainlinerate = [math]::Round(($cov.covered / $cov.total) * 100, 2) } } # ---- GET COVERAGE FROM LOCAL RUN --------------------------------------------- # Get current code coverage from coverage.xml file. $coveragePath = Get-ChildItem -Recurse -Filter "coverage.xml" $wd if (Test-Path -Path $coveragePath) { [xml]$BranchXML = Get-Content $coveragePath } else { Write-Host -ForegroundColor Red ` "No code coverage from this build. Is pytest configured to output code coverage? Exiting." exit 1 } $branchlinerate = [math]::Round([decimal]$BranchXML.coverage.'line-rate' * 100, 2) # ---- PRINT OUTPUT ------------------------------------------------------------ Write-Output "" Write-Output "Main coverage rate: $mainlinerate%" Write-Output "Branch coverage rate: $branchlinerate%" if ($mainlinerate -eq 0) { $change = "Infinite" } else { $change = [math]::Abs($branchlinerate - $mainlinerate) } if ($branchlinerate -gt $mainlinerate) { Write-Host "Coverage increased by $change% 😀" -ForegroundColor Green exit 0 } elseif ($branchlinerate -eq $mainlinerate) { Write-Host "Coverage has not changed." -ForegroundColor Green exit 0 } else { Write-Host "Coverage decreased by $change% 😭" -ForegroundColor Red exit 4 } django-sass-1.1.0/django_sass/000077500000000000000000000000001411024777400162245ustar00rootroot00000000000000django-sass-1.1.0/django_sass/__init__.py000066400000000000000000000110161411024777400203340ustar00rootroot00000000000000from typing import Dict, List import os from django.contrib.staticfiles.finders import get_finders import sass def find_static_paths() -> List[str]: """ Finds all static paths available in this Django project. :returns: List of paths containing static files. """ found_paths = [] for finder in get_finders(): if hasattr(finder, "storages"): for appname in finder.storages: if hasattr(finder.storages[appname], "location"): abspath = finder.storages[appname].location found_paths.append(abspath) return found_paths def find_static_scss() -> List[str]: """ Finds all static scss/sass files available in this Django project. :returns: List of paths of static scss/sass files. """ scss_files = [] for finder in get_finders(): for path, storage in finder.list([]): if path.endswith(".scss") or path.endswith(".sass"): fullpath = finder.find(path) scss_files.append(fullpath) return scss_files def compile_sass( inpath: str, outpath: str, output_style: str = None, precision: int = None, source_map: bool = False, include_paths: List[str] = None, ) -> None: """ Calls sass.compile() within context of Django's known static file paths, and writes output CSS and/or sourcemaps to file. :param str inpath: Path to SCSS/Sass file or directory of SCSS/Sass files. :param str outpath: Path to a CSS file or directory in which to write output. The path will be created if it does not exist. :param str output_style: Corresponds to `output_style` from sass package. :param int precision: Corresponds to `precision` from sass package. :param bool source_map: If True, write a source map along with the output CSS file. Only valid when `inpath` is a file. :returns: None """ # If include paths are not specified, use Django static paths include_paths = include_paths or find_static_paths() # Additional sass args that must be figured out. sassargs = {} # type: Dict[str, object] # Handle input directories. if os.path.isdir(inpath): # Assume outpath is also a dir, or make it. if not os.path.exists(outpath): os.makedirs(outpath) if os.path.isdir(outpath): sassargs.update({"dirname": (inpath, outpath)}) else: raise NotADirectoryError( "Output path must also be a directory when input path is a directory." ) # Handle input files. outfile = None if os.path.isfile(inpath): sassargs.update({"filename": inpath}) # If outpath does not exist, guess if it should be a dir and create it. if not os.path.exists(outpath): if not outpath.endswith(".css"): os.makedirs(outpath) # If outpath is a directory, create a child file. # Otherwise use provided file path. if os.path.exists(outpath) and os.path.isdir(outpath): outfile = os.path.join( outpath, os.path.basename( inpath.replace(".scss", ".css").replace(".sass", ".css") ), ) else: outfile = outpath # Create source map if specified. if source_map: sassargs.update({"source_map_filename": outfile + ".map"}) # Compile the sass. rval = sass.compile( output_style=output_style, precision=precision, include_paths=include_paths, **sassargs, ) # Write output. # sass.compile() will return None if used with dirname. # If used with filename, it will return a string of file contents. if rval and outfile: # If we got a css and sourcemap tuple, write the sourcemap. if isinstance(rval, tuple): map_outfile = outfile + ".map" outfile_dir = os.path.dirname(map_outfile) if not os.path.exists(outfile_dir): os.makedirs(outfile_dir, exist_ok=True) file = open(map_outfile, "w", encoding="utf8") file.write(rval[1]) file.close() rval = rval[0] # Write the outputted css to file. outfile_dir = os.path.dirname(outfile) if not os.path.exists(outfile_dir): os.makedirs(outfile_dir, exist_ok=True) file = open(outfile, "w", encoding="utf8") file.write(rval) file.close() django-sass-1.1.0/django_sass/apps.py000066400000000000000000000001401411024777400175340ustar00rootroot00000000000000from django.apps import AppConfig class DjangoSassConfig(AppConfig): name = "django_sass" django-sass-1.1.0/django_sass/management/000077500000000000000000000000001411024777400203405ustar00rootroot00000000000000django-sass-1.1.0/django_sass/management/commands/000077500000000000000000000000001411024777400221415ustar00rootroot00000000000000django-sass-1.1.0/django_sass/management/commands/sass.py000066400000000000000000000075321411024777400234730ustar00rootroot00000000000000from typing import Dict import os import sys import time from django.core.management.base import BaseCommand import sass from django_sass import compile_sass, find_static_scss class Command(BaseCommand): help = "Runs libsass including all paths from STATICFILES_FINDERS." def add_arguments(self, parser): parser.add_argument( "in", type=str, nargs="+", help="An scss file, or directory containing scss files", ) parser.add_argument( "out", type=str, nargs="+", help="A file or directory in which to output transpiled css", ) parser.add_argument( "-g", dest="g", action="store_true", help="Build a sourcemap. Only applicable if input is a file, not a directory.", ) parser.add_argument( "-t", type=str, dest="t", default="expanded", help="Output type. One of 'expanded', 'nested', 'compact', 'compressed'", ) parser.add_argument( "-p", type=int, dest="p", default=8, help="Precision. Defaults to 8", ) parser.add_argument( "--watch", dest="watch", action="store_true", default=False, help="Watch input path and re-generate css files when scss files are changed.", ) def handle(self, *args, **options) -> None: """ Finds all static paths used by the project, and runs sass including those paths. """ # Parse options. o_inpath = options["in"][0] o_outpath = options["out"][0] o_srcmap = options["g"] o_precision = options["p"] o_style = options["t"] # Watch files for changes if specified. if options["watch"]: try: self.stdout.write("Watching...") # Track list of files to watch and their modified time. watchfiles = {} # type: Dict[str, float] while True: needs_updated = False # Build/update list of ALL scss files in static paths. for fullpath in find_static_scss(): prev_mtime = watchfiles.get(fullpath, 0) curr_mtime = os.stat(fullpath).st_mtime if curr_mtime > prev_mtime: needs_updated = True watchfiles.update({fullpath: curr_mtime}) # Recompile the sass if needed. if needs_updated: # Catch compile errors to keep the watcher running. try: compile_sass( inpath=o_inpath, outpath=o_outpath, output_style=o_style, precision=o_precision, source_map=o_srcmap, ) self.stdout.write( "Updated files at %s" % time.time() ) except sass.CompileError as exc: self.stdout.write(str(exc)) # Go back to sleep. time.sleep(3) except (KeyboardInterrupt, InterruptedError): self.stdout.write("Bye.") sys.exit(0) # Write css. self.stdout.write("Writing css...") compile_sass( inpath=o_inpath, outpath=o_outpath, output_style=o_style, precision=o_precision, source_map=o_srcmap, ) self.stdout.write("Done.") django-sass-1.1.0/pyproject.toml000066400000000000000000000002601411024777400166430ustar00rootroot00000000000000[tool.black] line-length = 80 target-version = ['py36', 'py37', 'py38'] # Regular expression of files to exclude. exclude = ''' /( .venv | venv | migrations )/ ''' django-sass-1.1.0/requirements-dev.txt000066400000000000000000000001131411024777400177640ustar00rootroot00000000000000-e ./ black flake8 mypy pytest pytest-cov pytest-django sphinx twine wheel django-sass-1.1.0/setup.cfg000066400000000000000000000006341411024777400155550ustar00rootroot00000000000000[flake8] max-line-length = 100 exclude = .venv,venv,migrations [mypy] ignore_missing_imports = True namespace_packages = True [tool:pytest] DJANGO_SETTINGS_MODULE = testproject.settings junit_family = xunit2 addopts = --cov django_sass --cov-report html --cov-report xml --junitxml junit/test-results.xml ./testproject/ python_files = tests.py test_*.py filterwarnings = ignore default:::django_sass.* django-sass-1.1.0/setup.py000066400000000000000000000027111411024777400154440ustar00rootroot00000000000000import os from setuptools import setup, find_packages with open( os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf8" ) as readme: README = readme.read() setup( name="django-sass", version="1.1.0", author="CodeRed LLC", author_email="info@coderedcorp.com", url="https://github.com/coderedcorp/django-sass", description=( "The absolute simplest way to use Sass with Django. Pure Python, " "minimal dependencies, and no special configuration required!" ), long_description=README, long_description_content_type="text/markdown", license="BSD license", include_package_data=True, packages=find_packages(), install_requires=[ "django", "libsass", ], classifiers=[ "Environment :: Web Environment", "Framework :: Django :: 2.0", "Framework :: Django :: 2.1", "Framework :: Django :: 2.2", "Framework :: Django :: 3.0", "Framework :: Django :: 3.1", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", ], ) django-sass-1.1.0/testproject/000077500000000000000000000000001411024777400162775ustar00rootroot00000000000000django-sass-1.1.0/testproject/app1/000077500000000000000000000000001411024777400171405ustar00rootroot00000000000000django-sass-1.1.0/testproject/app1/__init__.py000066400000000000000000000000001411024777400212370ustar00rootroot00000000000000django-sass-1.1.0/testproject/app1/apps.py000066400000000000000000000001231411024777400204510ustar00rootroot00000000000000from django.apps import AppConfig class App1Config(AppConfig): name = "app1" django-sass-1.1.0/testproject/app1/static/000077500000000000000000000000001411024777400204275ustar00rootroot00000000000000django-sass-1.1.0/testproject/app1/static/app1/000077500000000000000000000000001411024777400212705ustar00rootroot00000000000000django-sass-1.1.0/testproject/app1/static/app1/scss/000077500000000000000000000000001411024777400222435ustar00rootroot00000000000000django-sass-1.1.0/testproject/app1/static/app1/scss/_include.scss000066400000000000000000000001101411024777400247120ustar00rootroot00000000000000/* Tests: app1/scss/_include.scss */ .app1-include { color: red; } django-sass-1.1.0/testproject/app2/000077500000000000000000000000001411024777400171415ustar00rootroot00000000000000django-sass-1.1.0/testproject/app2/__init__.py000066400000000000000000000000001411024777400212400ustar00rootroot00000000000000django-sass-1.1.0/testproject/app2/apps.py000066400000000000000000000001231411024777400204520ustar00rootroot00000000000000from django.apps import AppConfig class App2Config(AppConfig): name = "app2" django-sass-1.1.0/testproject/app2/static/000077500000000000000000000000001411024777400204305ustar00rootroot00000000000000django-sass-1.1.0/testproject/app2/static/app2/000077500000000000000000000000001411024777400212725ustar00rootroot00000000000000django-sass-1.1.0/testproject/app2/static/app2/scss/000077500000000000000000000000001411024777400222455ustar00rootroot00000000000000django-sass-1.1.0/testproject/app2/static/app2/scss/_samedir.scss000066400000000000000000000001101411024777400247150ustar00rootroot00000000000000/* Tests: app2/scss/_samedir.scss */ .app2-samedir { color: red; } django-sass-1.1.0/testproject/app2/static/app2/scss/subdir/000077500000000000000000000000001411024777400235355ustar00rootroot00000000000000django-sass-1.1.0/testproject/app2/static/app2/scss/subdir/_subdir.scss000066400000000000000000000001151411024777400260560ustar00rootroot00000000000000/* Tests: app2/scss/subdir/_subdir.scss */ .app2-subdir { color: red; } django-sass-1.1.0/testproject/app2/static/app2/scss/test.scss000066400000000000000000000005131411024777400241200ustar00rootroot00000000000000// app1/scss/_include.scss @import "app1/scss/include"; // app2/_samedir.scss @import "app2/scss/samedir"; // app2/subdir/_subdir.scss @import "app2/scss/subdir/subdir"; // _samedir.scss @import "samedir"; // subdir/_subdir.scss @import "subdir/subdir"; // test.scss /* Tests: app2/scss/test.scss */ .test { color: red; } django-sass-1.1.0/testproject/app3/000077500000000000000000000000001411024777400171425ustar00rootroot00000000000000django-sass-1.1.0/testproject/app3/__init__.py000066400000000000000000000000001411024777400212410ustar00rootroot00000000000000django-sass-1.1.0/testproject/app3/apps.py000066400000000000000000000001231411024777400204530ustar00rootroot00000000000000from django.apps import AppConfig class App3Config(AppConfig): name = "app3" django-sass-1.1.0/testproject/app3/static/000077500000000000000000000000001411024777400204315ustar00rootroot00000000000000django-sass-1.1.0/testproject/app3/static/app3/000077500000000000000000000000001411024777400212745ustar00rootroot00000000000000django-sass-1.1.0/testproject/app3/static/app3/sass/000077500000000000000000000000001411024777400222455ustar00rootroot00000000000000django-sass-1.1.0/testproject/app3/static/app3/sass/indent_test.sass000066400000000000000000000001161411024777400254560ustar00rootroot00000000000000/* Tests: app3/sass/indent_test.sass */ .test_item border: 1px solid red django-sass-1.1.0/testproject/manage.py000066400000000000000000000011671411024777400201060ustar00rootroot00000000000000#!/usr/bin/env python """Django's command-line utility for administrative tasks.""" import os import sys def main(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) if __name__ == "__main__": main() django-sass-1.1.0/testproject/requirements.txt000066400000000000000000000000441411024777400215610ustar00rootroot00000000000000-e ../ # django-sass locally django django-sass-1.1.0/testproject/testproject/000077500000000000000000000000001411024777400206455ustar00rootroot00000000000000django-sass-1.1.0/testproject/testproject/__init__.py000066400000000000000000000000001411024777400227440ustar00rootroot00000000000000django-sass-1.1.0/testproject/testproject/settings.py000066400000000000000000000051611411024777400230620ustar00rootroot00000000000000""" Django settings for testproject project. Generated by 'django-admin startproject' using Django 2.2.1. For more information on this file, see https://docs.djangoproject.com/en/2.2/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/2.2/ref/settings/ """ import os from typing import List # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "-_wl=tq26(*wyvfza+ncg_436c53pu81d=07j62+vm5y2pc)f^" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # type: List[str] # Application definition INSTALLED_APPS = [ "app1", "app2", "app3", "django_sass", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] ROOT_URLCONF = "testproject.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ], }, }, ] WSGI_APPLICATION = "testproject.wsgi.application" # Database # https://docs.djangoproject.com/en/2.2/ref/settings/#databases DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ STATIC_URL = "/static/" django-sass-1.1.0/testproject/testproject/urls.py000066400000000000000000000013611411024777400222050ustar00rootroot00000000000000"""testproject URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/2.2/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin from django.urls import path urlpatterns = [ path("admin/", admin.site.urls), ] django-sass-1.1.0/testproject/testproject/wsgi.py000066400000000000000000000006171411024777400221740ustar00rootroot00000000000000""" WSGI config for testproject project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings") application = get_wsgi_application() django-sass-1.1.0/testproject/tests.py000066400000000000000000000142621411024777400200200ustar00rootroot00000000000000import os import shutil import subprocess import time import unittest from typing import List from django_sass import find_static_paths, find_static_scss THIS_DIR = os.path.dirname(os.path.abspath(__file__)) SCSS_CONTAINS = [ "/* Tests: app1/scss/_include.scss */", "/* Tests: app2/scss/_samedir.scss */", "/* Tests: app2/scss/subdir/_subdir.scss */", "/* Tests: app2/scss/test.scss */", ] class TestDjangoSass(unittest.TestCase): def setUp(self): self.outdir = os.path.join(THIS_DIR, "out") def tearDown(self): # Clean up output files shutil.rmtree(self.outdir, ignore_errors=True) def assert_output( self, inpath: str, outpath: str, real_outpath: str, contains: List[str], args: List[str] = None, ): # Command to run args = args or [] cmd = ["python", "manage.py", "sass", *args, inpath, outpath] # Run the management command on testproject. proc = subprocess.run(cmd, cwd=THIS_DIR) # Verify the process exited cleanly. self.assertEqual(proc.returncode, 0) # Verify that the output file exists. # self.assertTrue(os.path.isfile(real_outpath)) # Verify that the file contains expected output from all sass files. with open(real_outpath, encoding="utf8") as f: contents = f.read() for compiled_data in contains: self.assertTrue(compiled_data in contents) def test_find_static_paths(self): paths = find_static_paths() # Assert that it found both of our apps' static dirs. self.assertTrue(os.path.join(THIS_DIR, "app1", "static") in paths) self.assertTrue(os.path.join(THIS_DIR, "app2", "static") in paths) def test_find_static_sass(self): files = find_static_scss() # Assert that it found all of our scss files. self.assertTrue( os.path.join( THIS_DIR, "app1", "static", "app1", "scss", "_include.scss" ) in files ) self.assertTrue( os.path.join( THIS_DIR, "app2", "static", "app2", "scss", "_samedir.scss" ) in files ) self.assertTrue( os.path.join( THIS_DIR, "app2", "static", "app2", "scss", "test.scss" ) in files ) self.assertTrue( os.path.join( THIS_DIR, "app2", "static", "app2", "scss", "subdir", "_subdir.scss", ) in files ) self.assertTrue( os.path.join( THIS_DIR, "app3", "static", "app3", "sass", "indent_test.sass" ) in files ) def test_cli(self): # Input and output paths relative to django static dirs. inpath = os.path.join("app2", "static", "app2", "scss", "test.scss") outpath = os.path.join(self.outdir, "test_file.css") self.assert_output( inpath=inpath, outpath=outpath, real_outpath=outpath, contains=SCSS_CONTAINS, ) def test_cli_dir(self): # Input and output paths relative to django static dirs. inpath = os.path.join("app2", "static", "app2", "scss") # Expected output path on filesystem. real_outpath = os.path.join(self.outdir, "test.css") self.assert_output( inpath=inpath, outpath=self.outdir, real_outpath=real_outpath, contains=SCSS_CONTAINS, ) def test_cli_infile_outdir(self): # Input is a file; output is non-existant path (without .css extension). inpath = os.path.join("app2", "static", "app2", "scss", "test.scss") outpath = os.path.join(self.outdir, "does-not-exist") # Expected output path on filesystem. real_outpath = os.path.join(outpath, "test.css") self.assert_output( inpath=inpath, outpath=outpath, real_outpath=real_outpath, contains=SCSS_CONTAINS, ) def test_sass_compiles(self): # Input and output paths relative to django static dirs. inpath = os.path.join("app3", "static", "app3", "sass") # Expected output path on filesystem. real_outpath = os.path.join(self.outdir, "indent_test.css") self.assert_output( inpath=inpath, outpath=self.outdir, real_outpath=real_outpath, contains=["/* Tests: app3/sass/indent_test.sass */"], ) def test_cli_srcmap(self): # Input and output paths relative to django static dirs. inpath = os.path.join("app2", "static", "app2", "scss", "test.scss") outpath = os.path.join(self.outdir, "test.css") self.assert_output( inpath=inpath, outpath=outpath, real_outpath=outpath, contains=SCSS_CONTAINS, args=["-g"], ) self.assertTrue( os.path.isfile(os.path.join(self.outdir, "test.css.map")) ) @unittest.skip("Test needs fixed...") def test_cli_watch(self): # Input and output paths relative to django static dirs. inpath = os.path.join("app2", "static", "app2", "scss", "test.scss") outpath = os.path.join(self.outdir, "test.css") # Command to run cmd = ["python", "manage.py", "sass", "--watch", inpath, outpath] # Run the management command on testproject. proc = subprocess.Popen(cmd, cwd=THIS_DIR) time.sleep(0.5) # TODO: This test is not working. Do not know how to intentionally send # a KeyboardInterrupt to the subprocess without having unittest/pytest # immediately die when it sees the interrupt. try: proc.send_signal(subprocess.signal.CTRL_C_EVENT) except KeyboardInterrupt: # We actually want the keyboard interrupt. pass returncode = proc.wait() # Verify the process exited cleanly. self.assertEqual(returncode, 0) # Assert output is correct. self.assert_output(outpath)