pax_global_header00006660000000000000000000000064136235645270014527gustar00rootroot0000000000000052 comment=ac56a1e73f3ff3eca507e18238648643a32f03ff django-sass-1.0.0/000077500000000000000000000000001362356452700137365ustar00rootroot00000000000000django-sass-1.0.0/.gitattributes000066400000000000000000000004211362356452700166260ustar00rootroot00000000000000# 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.0.0/.gitignore000066400000000000000000000127521362356452700157350ustar00rootroot00000000000000# 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.0.0/LICENSE000066400000000000000000000027731362356452700147540ustar00rootroot00000000000000BSD 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.0.0/MANIFEST.in000066400000000000000000000001461362356452700154750ustar00rootroot00000000000000include LICENSE *.rst *.txt *.md graft django_sass global-exclude __pycache__ global-exclude *.py[co] django-sass-1.0.0/README.md000066400000000000000000000173551362356452700152300ustar00rootroot00000000000000django-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/coderedcms/_apis/build/status/django-sass?branchName=master)](https://dev.azure.com/coderedcorp/coderedcms/_build/latest?definitionId=10&branchName=master) [![Azure DevOps tests (branch)](https://img.shields.io/azure-devops/tests/coderedcorp/coderedcms/10/master)](https://dev.azure.com/coderedcorp/coderedcms/_build/latest?definitionId=10&branchName=master) [![Azure DevOps coverage (branch)](https://img.shields.io/azure-devops/coverage/coderedcorp/coderedcms/10/master)](https://dev.azure.com/coderedcorp/coderedcms/_build/latest?definitionId=10&branchName=master) | 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 files ending in `.scss` are supported for now. * 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. * 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)$ flake8 (myvenv)$ mypy ``` Then run the unit tests: ``` (myvenv)$ pytest ``` Changelog --------- #### 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.0.0/azure-pipelines.yml000066400000000000000000000055371362356452700176070ustar00rootroot00000000000000# 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: - master stages: - stage: Unit_Tests displayName: Unit Tests jobs: - job: pytest displayName: pytest pool: vmImage: 'ubuntu-latest' strategy: matrix: py3.5: PYTHON_VERSION: '3.5' py3.6: PYTHON_VERSION: '3.6' py3.7: PYTHON_VERSION: '3.7' py3.8: PYTHON_VERSION: '3.8' 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.8' 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: 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.0.0/ci/000077500000000000000000000000001362356452700143315ustar00rootroot00000000000000django-sass-1.0.0/ci/compare-codecov.ps1000066400000000000000000000057371362356452700200400ustar00rootroot00000000000000#!/usr/bin/env pwsh <# .SYNOPSIS Compares code coverage percent of local coverage.xml file to master 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 = "coderedcms", [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. $masterBuildJson = ( Invoke-WebRequest "$ApiBase/_apis/build/builds?branchName=refs/heads/master&api-version=5.1" ).Content | ConvertFrom-Json # Get the latest matching build ID from the list of builds. foreach ($build in $masterBuildJson.value) { if ($build.definition.name -eq $pipeline_name) { $masterLatestId = $build.id break } } # Retrieve code coverage for this build ID. $masterCoverageJson = ( Invoke-WebRequest "$ApiBase/_apis/test/codecoverage?buildId=$masterLatestId&api-version=5.1-preview.1" ).Content | ConvertFrom-Json foreach ($cov in $masterCoverageJson.coverageData.coverageStats) { if ($cov.label -eq "Lines") { $masterlinerate = [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 "Master line coverage rate: $masterlinerate%" Write-Output "Branch line coverage rate: $branchlinerate%" if ($masterlinerate -eq 0) { $change = "Infinite" } else { $change = [math]::Abs($branchlinerate - $masterlinerate) } if ($branchlinerate -gt $masterlinerate) { Write-Host "Coverage increased by $change% 😀" -ForegroundColor Green exit 0 } elseif ($branchlinerate -eq $masterlinerate) { Write-Host "Coverage has not changed." -ForegroundColor Green exit 0 } else { Write-Host "Coverage decreased by $change% 😭" -ForegroundColor Red exit 4 } django-sass-1.0.0/django_sass/000077500000000000000000000000001362356452700162315ustar00rootroot00000000000000django-sass-1.0.0/django_sass/__init__.py000066400000000000000000000100141362356452700203360ustar00rootroot00000000000000from 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 files available in this Django project. :returns: List of paths of static scss files. """ scss_files = [] for finder in get_finders(): for path, storage in finder.list([]): if path.endswith(".scss"): 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 file or directory of SCSS 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 os.path.isdir(outpath): outfile = os.path.join( outpath, os.path.basename(inpath.replace(".scss", ".css")) ) else: outfile = outpath 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.0.0/django_sass/apps.py000066400000000000000000000001401362356452700175410ustar00rootroot00000000000000from django.apps import AppConfig class DjangoSassConfig(AppConfig): name = 'django_sass' django-sass-1.0.0/django_sass/management/000077500000000000000000000000001362356452700203455ustar00rootroot00000000000000django-sass-1.0.0/django_sass/management/commands/000077500000000000000000000000001362356452700221465ustar00rootroot00000000000000django-sass-1.0.0/django_sass/management/commands/sass.py000066400000000000000000000073521362356452700235000ustar00rootroot00000000000000import 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 = {} 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.0.0/requirements-dev.txt000066400000000000000000000001051362356452700177720ustar00rootroot00000000000000-e ./ flake8 mypy pytest pytest-cov pytest-django sphinx twine wheel django-sass-1.0.0/setup.cfg000066400000000000000000000005671362356452700155670ustar00rootroot00000000000000[flake8] max-line-length = 100 exclude = migrations [mypy] ignore_missing_imports = 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.0.0/setup.py000066400000000000000000000023231362356452700154500ustar00rootroot00000000000000import 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.0.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=[ "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Framework :: Django", "Framework :: Django :: 2.0", "Framework :: Django :: 2.1", "Framework :: Django :: 2.2", "Framework :: Django :: 3.0", "Environment :: Web Environment", ], ) django-sass-1.0.0/testproject/000077500000000000000000000000001362356452700163045ustar00rootroot00000000000000django-sass-1.0.0/testproject/app1/000077500000000000000000000000001362356452700171455ustar00rootroot00000000000000django-sass-1.0.0/testproject/app1/__init__.py000066400000000000000000000000001362356452700212440ustar00rootroot00000000000000django-sass-1.0.0/testproject/app1/apps.py000066400000000000000000000001231362356452700204560ustar00rootroot00000000000000from django.apps import AppConfig class App1Config(AppConfig): name = 'app1' django-sass-1.0.0/testproject/app1/static/000077500000000000000000000000001362356452700204345ustar00rootroot00000000000000django-sass-1.0.0/testproject/app1/static/app1/000077500000000000000000000000001362356452700212755ustar00rootroot00000000000000django-sass-1.0.0/testproject/app1/static/app1/scss/000077500000000000000000000000001362356452700222505ustar00rootroot00000000000000django-sass-1.0.0/testproject/app1/static/app1/scss/_include.scss000066400000000000000000000001101362356452700247170ustar00rootroot00000000000000/* Tests: app1/scss/_include.scss */ .app1-include { color: red; } django-sass-1.0.0/testproject/app2/000077500000000000000000000000001362356452700171465ustar00rootroot00000000000000django-sass-1.0.0/testproject/app2/__init__.py000066400000000000000000000000001362356452700212450ustar00rootroot00000000000000django-sass-1.0.0/testproject/app2/apps.py000066400000000000000000000001231362356452700204570ustar00rootroot00000000000000from django.apps import AppConfig class App2Config(AppConfig): name = 'app2' django-sass-1.0.0/testproject/app2/static/000077500000000000000000000000001362356452700204355ustar00rootroot00000000000000django-sass-1.0.0/testproject/app2/static/app2/000077500000000000000000000000001362356452700212775ustar00rootroot00000000000000django-sass-1.0.0/testproject/app2/static/app2/scss/000077500000000000000000000000001362356452700222525ustar00rootroot00000000000000django-sass-1.0.0/testproject/app2/static/app2/scss/_samedir.scss000066400000000000000000000001101362356452700247220ustar00rootroot00000000000000/* Tests: app2/scss/_samedir.scss */ .app2-samedir { color: red; } django-sass-1.0.0/testproject/app2/static/app2/scss/subdir/000077500000000000000000000000001362356452700235425ustar00rootroot00000000000000django-sass-1.0.0/testproject/app2/static/app2/scss/subdir/_subdir.scss000066400000000000000000000001151362356452700260630ustar00rootroot00000000000000/* Tests: app2/scss/subdir/_subdir.scss */ .app2-subdir { color: red; } django-sass-1.0.0/testproject/app2/static/app2/scss/test.scss000066400000000000000000000005131362356452700241250ustar00rootroot00000000000000// 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.0.0/testproject/manage.py000066400000000000000000000011671362356452700201130ustar00rootroot00000000000000#!/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.0.0/testproject/requirements.txt000066400000000000000000000000441362356452700215660ustar00rootroot00000000000000-e ../ # django-sass locally django django-sass-1.0.0/testproject/testproject/000077500000000000000000000000001362356452700206525ustar00rootroot00000000000000django-sass-1.0.0/testproject/testproject/__init__.py000066400000000000000000000000001362356452700227510ustar00rootroot00000000000000django-sass-1.0.0/testproject/testproject/settings.py000066400000000000000000000050731362356452700230710ustar00rootroot00000000000000""" 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 # 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 = [] # Application definition INSTALLED_APPS = [ 'app1', 'app2', '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.0.0/testproject/testproject/urls.py000066400000000000000000000013611362356452700222120ustar00rootroot00000000000000"""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.0.0/testproject/testproject/wsgi.py000066400000000000000000000006171362356452700222010ustar00rootroot00000000000000""" 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.0.0/testproject/tests.py000066400000000000000000000115341362356452700200240ustar00rootroot00000000000000import os import shutil import subprocess import time import unittest from django_sass import find_static_paths, find_static_scss THIS_DIR = os.path.dirname(os.path.abspath(__file__)) 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, real_outpath: str): # Verify that the output file exists. print(real_outpath) 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() self.assertTrue("/* Tests: app1/scss/_include.scss */" in contents) self.assertTrue("/* Tests: app2/scss/_samedir.scss */" in contents) self.assertTrue("/* Tests: app2/scss/subdir/_subdir.scss */" in contents) self.assertTrue("/* Tests: app2/scss/test.scss */" 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) 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") # Command to run cmd = ["python", "manage.py", "sass", 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) # Assert output is correct. self.assert_output(outpath) def test_cli_dir(self): # Input and output paths relative to django static dirs. inpath = os.path.join("app2", "static", "app2", "scss") outpath = self.outdir # Expected output path on filesystem. real_outpath = os.path.join(self.outdir, "test.css") # Command to run cmd = ["python", "manage.py", "sass", 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) # Assert output is correct. self.assert_output(real_outpath) 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") # Command to run cmd = ["python", "manage.py", "sass", "-g", 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) # Assert output is correct. self.assert_output(outpath) self.assertTrue(os.path.isfile(os.path.join(self.outdir, "test.css.map"))) @unittest.skip 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)