├── testpkg ├── testpkg │ ├── __init__.py │ └── static │ │ ├── css │ │ └── README │ │ └── scss │ │ └── a.scss └── setup.py ├── test ├── _f.scss ├── h.sass ├── e.scss ├── b.scss ├── subdir │ ├── recur.scss │ └── _sub.scss ├── c.scss ├── a.scss ├── d.scss └── g.scss ├── docs ├── sass.rst ├── sassutils │ ├── wsgi.rst │ ├── builder.rst │ └── distutils.rst ├── pysassc.rst ├── sassutils.rst ├── index.rst ├── make.bat ├── frameworks │ └── flask.rst ├── Makefile ├── conf.py └── changes.rst ├── .ackrc ├── .gitmodules ├── requirements-dev.txt ├── .gitignore ├── MANIFEST.in ├── sassutils ├── __init__.py ├── wsgi.py ├── distutils.py └── builder.py ├── tox.ini ├── CODE_OF_CONDUCT.md ├── .coveragerc ├── .github └── workflows │ └── main.yml ├── LICENSE ├── .pre-commit-config.yaml ├── bin └── build-manylinux-wheels ├── setup.cfg ├── CONTRIBUTING.rst ├── README.rst ├── setup.py ├── pysassc.py ├── _sass.c └── sass.py /testpkg/testpkg/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/_f.scss: -------------------------------------------------------------------------------- 1 | $test-variable : true !default; 2 | -------------------------------------------------------------------------------- /test/h.sass: -------------------------------------------------------------------------------- 1 | a 2 | b 3 | color: blue 4 | -------------------------------------------------------------------------------- /docs/sass.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: sass 2 | :members: 3 | -------------------------------------------------------------------------------- /test/e.scss: -------------------------------------------------------------------------------- 1 | @import "f"; 2 | @import "subdir/sub"; 3 | -------------------------------------------------------------------------------- /test/b.scss: -------------------------------------------------------------------------------- 1 | b { 2 | i { 3 | font-size: 20px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/subdir/recur.scss: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | p { color: blue; } 4 | } 5 | -------------------------------------------------------------------------------- /docs/sassutils/wsgi.rst: -------------------------------------------------------------------------------- 1 | 2 | .. automodule:: sassutils.wsgi 3 | :members: 4 | -------------------------------------------------------------------------------- /.ackrc: -------------------------------------------------------------------------------- 1 | --ignore-dir=.tox 2 | --ignore-dir=build 3 | --ignore-dir=libsass.egg-info 4 | -------------------------------------------------------------------------------- /docs/sassutils/builder.rst: -------------------------------------------------------------------------------- 1 | 2 | .. automodule:: sassutils.builder 3 | :members: 4 | -------------------------------------------------------------------------------- /docs/sassutils/distutils.rst: -------------------------------------------------------------------------------- 1 | 2 | .. automodule:: sassutils.distutils 3 | :members: 4 | -------------------------------------------------------------------------------- /test/c.scss: -------------------------------------------------------------------------------- 1 | @import 'a.scss'; 2 | 3 | h1 { 4 | a { 5 | color: green; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libsass"] 2 | path = libsass 3 | url = https://github.com/sass/libsass 4 | -------------------------------------------------------------------------------- /docs/pysassc.rst: -------------------------------------------------------------------------------- 1 | 2 | .. program:: pysassc 3 | 4 | .. automodule:: pysassc 5 | :members: 6 | -------------------------------------------------------------------------------- /test/subdir/_sub.scss: -------------------------------------------------------------------------------- 1 | @if $test-variable == true { 2 | a { 3 | color: red; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /testpkg/testpkg/static/css/README: -------------------------------------------------------------------------------- 1 | NOTE: Do not delete this file; it's for preserving the directory in Git. 2 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | coverage-enable-subprocess 3 | pre-commit 4 | pytest 5 | setuptools 6 | werkzeug>=0.9 7 | -------------------------------------------------------------------------------- /testpkg/testpkg/static/scss/a.scss: -------------------------------------------------------------------------------- 1 | p { 2 | a { 3 | color: red; 4 | } 5 | b { 6 | color: blue; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/a.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin mx { 3 | background-color: green; 4 | } 5 | 6 | body { 7 | @include mx; 8 | a { 9 | color: blue; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/d.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin mx { 3 | background-color: green; 4 | } 5 | 6 | body { 7 | @include mx; 8 | a { 9 | font: '나눔고딕', sans-serif; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/sassutils.rst: -------------------------------------------------------------------------------- 1 | 2 | .. automodule:: sassutils 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | sassutils/builder 8 | sassutils/distutils 9 | sassutils/wsgi 10 | -------------------------------------------------------------------------------- /test/g.scss: -------------------------------------------------------------------------------- 1 | $font-stack: Helvetica, sans-serif; 2 | $primary-color: #333; 3 | $variabile: 5 / 3 * 6 / 7; 4 | 5 | body { 6 | font: 100% $font-stack; 7 | color: $primary-color; 8 | height: $variabile; 9 | } 10 | -------------------------------------------------------------------------------- /testpkg/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | name='testpkg', 6 | packages=['testpkg'], 7 | sass_manifests={ 8 | 'testpkg': ('static/scss', 'static/css'), 9 | }, 10 | setup_requires=['libsass'], 11 | ) 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg 2 | *.egg-info 3 | *.pyc 4 | *.pyd 5 | *.so 6 | .*.swp 7 | .DS_Store 8 | ._.DS_Store 9 | .coverage 10 | .tox 11 | /.libsass-upstream-version 12 | build/ 13 | dist/ 14 | docs/_build 15 | testpkg/build/ 16 | testpkg/dist/ 17 | testpkg/testpkg/static/css/*.scss.css 18 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include libsass *.c 2 | recursive-include libsass *.cpp 3 | recursive-include libsass *.h 4 | recursive-include libsass *.hpp 5 | include libsass/Makefile 6 | include .libsass-upstream-version 7 | include test/*.scss 8 | include README.rst 9 | include LICENSE 10 | -------------------------------------------------------------------------------- /sassutils/__init__.py: -------------------------------------------------------------------------------- 1 | """:mod:`sassutils` --- Additional utilities related to Sass 2 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | 4 | This package provides several additional utilities related to Sass 5 | which depends on libsass core (:mod:`sass` module). 6 | 7 | """ 8 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py,pypy3,pre-commit 3 | 4 | [testenv] 5 | usedevelop = true 6 | deps = -rrequirements-dev.txt 7 | setenv = PWD={toxinidir} 8 | commands = 9 | coverage erase 10 | coverage run -m pytest sasstests.py 11 | coverage combine 12 | coverage report 13 | 14 | [testenv:pre-commit] 15 | skip_install = true 16 | deps = pre-commit 17 | commands = pre-commit run --all-files --show-diff-on-failure 18 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Sass is more than a technology; Sass is driven by the community of individuals 2 | that power its development and use every day. As a community, we want to embrace 3 | the very differences that have made our collaboration so powerful, and work 4 | together to provide the best environment for learning, growing, and sharing of 5 | ideas. It is imperative that we keep Sass a fun, welcoming, challenging, and 6 | fair place to play. 7 | 8 | [The full community guidelines can be found on the Sass website.][link] 9 | 10 | [link]: https://sass-lang.com/community-guidelines 11 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | parallel = True 3 | branch = True 4 | source = $PWD 5 | data_file = $PWD/.coverage 6 | omit = 7 | */.tox/* 8 | /usr/* 9 | */setup.py 10 | 11 | [report] 12 | show_missing = True 13 | exclude_lines = 14 | # Have to re-enable the standard pragma 15 | \#\s*pragma: no cover 16 | 17 | # Don't complain if tests don't hit defensive assertion code: 18 | ^\s*raise AssertionError\b 19 | ^\s*raise NotImplementedError\b 20 | ^\s*return NotImplemented\b 21 | ^\s*raise TypeError\b 22 | ^\s*raise$ 23 | 24 | # Don't complain if non-runnable code isn't run: 25 | ^if __name__ == ['"]__main__['"]:$ 26 | 27 | [html] 28 | directory = coverage-html 29 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: [main, test-me-*] 6 | tags: '*' 7 | pull_request: 8 | 9 | jobs: 10 | main-windows: 11 | uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 12 | with: 13 | env: '["py310"]' 14 | os: windows-latest 15 | arch: '["x64", "x86"]' 16 | wheel-tags: true 17 | submodules: true 18 | main-macos: 19 | uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 20 | with: 21 | env: '["py310"]' 22 | os: macos-latest 23 | wheel-tags: true 24 | submodules: true 25 | main-macos-intel: 26 | uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 27 | with: 28 | env: '["py310"]' 29 | os: macos-13 30 | wheel-tags: true 31 | submodules: true 32 | main-linux: 33 | uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 34 | with: 35 | env: '["py310", "py311", "py312"]' 36 | os: ubuntu-latest 37 | submodules: true 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Hong Minhee 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v6.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: debug-statements 9 | - id: double-quote-string-fixer 10 | - id: name-tests-test 11 | - id: requirements-txt-fixer 12 | - repo: https://github.com/asottile/setup-cfg-fmt 13 | rev: v3.1.0 14 | hooks: 15 | - id: setup-cfg-fmt 16 | - repo: https://github.com/asottile/reorder-python-imports 17 | rev: v3.16.0 18 | hooks: 19 | - id: reorder-python-imports 20 | args: [--py310-plus] 21 | - repo: https://github.com/asottile/add-trailing-comma 22 | rev: v4.0.0 23 | hooks: 24 | - id: add-trailing-comma 25 | - repo: https://github.com/asottile/pyupgrade 26 | rev: v3.21.0 27 | hooks: 28 | - id: pyupgrade 29 | args: [--py310-plus] 30 | - repo: https://github.com/hhatto/autopep8 31 | rev: v2.3.2 32 | hooks: 33 | - id: autopep8 34 | - repo: https://github.com/PyCQA/flake8 35 | rev: 7.3.0 36 | hooks: 37 | - id: flake8 38 | exclude: ^docs/conf.py 39 | -------------------------------------------------------------------------------- /bin/build-manylinux-wheels: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Script for building 'manylinux' wheels for libsass. 3 | 4 | Run me after putting the source distribution on pypi. 5 | 6 | See: https://www.python.org/dev/peps/pep-0513/ 7 | """ 8 | import os 9 | import pipes 10 | import subprocess 11 | import tempfile 12 | 13 | 14 | def check_call(*cmd): 15 | print( 16 | 'build-manylinux-wheels>> ' + 17 | ' '.join(pipes.quote(part) for part in cmd), 18 | ) 19 | subprocess.check_call(cmd) 20 | 21 | 22 | def main(): 23 | os.makedirs('dist', exist_ok=True) 24 | with tempfile.TemporaryDirectory() as work: 25 | pip = '/opt/python/cp39-cp39/bin/pip' 26 | check_call( 27 | 'docker', 'run', '-ti', 28 | # Use this so the files are not owned by root 29 | '--user', f'{os.getuid()}:{os.getgid()}', 30 | # We'll do building in /work and copy results to /dist 31 | '-v', f'{work}:/work:rw', 32 | '-v', '{}:/dist:rw'.format(os.path.abspath('dist')), 33 | 'quay.io/pypa/manylinux1_x86_64:latest', 34 | 'bash', '-exc', 35 | '{} wheel --verbose --wheel-dir /work --no-deps libsass && ' 36 | 'auditwheel repair --wheel-dir /dist /work/*.whl'.format(pip), 37 | ) 38 | return 0 39 | 40 | 41 | if __name__ == '__main__': 42 | raise SystemExit(main()) 43 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = libsass 3 | version = attr: sass.__version__ 4 | description = Sass for Python: A straightforward binding of libsass for Python. 5 | long_description = file: README.rst 6 | long_description_content_type = text/x-rst 7 | url = https://sass.github.io/libsass-python/ 8 | author = Hong Minhee 9 | author_email = minhee@dahlia.kr 10 | license = MIT 11 | license_files = LICENSE 12 | classifiers = 13 | Development Status :: 5 - Production/Stable 14 | Environment :: Web Environment 15 | Intended Audience :: Developers 16 | Operating System :: OS Independent 17 | Programming Language :: C 18 | Programming Language :: C++ 19 | Programming Language :: Python :: 3 20 | Programming Language :: Python :: 3 :: Only 21 | Programming Language :: Python :: Implementation :: CPython 22 | Programming Language :: Python :: Implementation :: PyPy 23 | Programming Language :: Python :: Implementation :: Stackless 24 | Topic :: Internet :: WWW/HTTP 25 | Topic :: Internet :: WWW/HTTP :: Dynamic Content 26 | Topic :: Software Development :: Code Generators 27 | Topic :: Software Development :: Compilers 28 | 29 | [options] 30 | packages = sassutils 31 | py_modules = 32 | pysassc 33 | sass 34 | sasstests 35 | python_requires = >=3.10 36 | 37 | [options.entry_points] 38 | console_scripts = 39 | pysassc = pysassc:main 40 | distutils.commands = 41 | build_sass = sassutils.distutils:build_sass 42 | distutils.setup_keywords = 43 | sass_manifests = sassutils.distutils:validate_manifests 44 | 45 | [aliases] 46 | upload_doc = build_sphinx upload_doc 47 | release = sdist upload build_sphinx upload_doc 48 | 49 | [flake8] 50 | exclude = .tox,build,dist,docs,ez_setup.py 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributor's guide 2 | =================== 3 | 4 | Coding style 5 | ------------ 6 | 7 | - Follow `PEP 8`_. flake8_ would help. 8 | - Order imports by lexicographical order. 9 | - Prefer relative imports. 10 | - All functions, classes, methods, attributes, and modules should have 11 | the docstring. 12 | - Functions and methods should contain ``:param:``, ``:type:`` 13 | (``:return:``, ``:rtype`` if it returns something), 14 | (``:raise:`` if it may raise an error) in their docstring. 15 | 16 | .. _flake8: https://gitlab.com/pycqa/flake8 17 | .. _PEP 8: https://www.python.org/dev/peps/pep-0008 18 | 19 | 20 | Tests 21 | ----- 22 | 23 | - All code patches should contain one or more unit tests or regression tests. 24 | - All code patches have to successfully run tests on every Python version 25 | we aim to support. tox_ would help. 26 | - All commits will be tested by `GitHub Actions`_ (Linux and Windows). 27 | 28 | .. _tox: https://tox.readthedocs.io/ 29 | .. _`GitHub Actions`: https://github.com/sass/libsass-python/actions 30 | 31 | 32 | Maintainer's guide 33 | ================== 34 | 35 | Releasing 36 | --------- 37 | 38 | Here's a brief check list for releasing a new version: 39 | 40 | - Double check if the version is correctly bumped. 41 | You can bump the version by changing ``__version__`` in sass.py file. 42 | Note that it might be already bumped by other maintainers, 43 | so check what's the latest release version from PyPI_. 44 | - The changelog has to be complete, and frozen. 45 | "To be released" sentence has to be replaced by the actual release date. 46 | - If the code freeze for the release is done (including version bump), 47 | tag the commit using ``git tag`` command. The tag name has to simply be 48 | the version name e.g. ``1.2.3``. Of course, the tag also has to be pushed 49 | to the upstream repository. 50 | - Make a source distribution and upload it to PyPI 51 | (``python3 setup.py sdist upload``). 52 | If it's successful the new version must appear on PyPI_. 53 | - `GitHub Actions`_ automatically makes binary wheels for Windows, but each 54 | CI build takes a while. These wheels are not automatically uploaded, 55 | you can retreive them from the build's artifacts. 56 | - Run ``./bin/build-manylinux-wheels`` to build linux wheels and upload them to 57 | PyPI (takes ~5 minutes). 58 | - The `docs website`__ also has to be updated. 59 | It's currently a static website deployed on GitHub Pages. 60 | Use ``python setup.py upload_doc`` command. 61 | Although it seems possible to be automated using Github Actions. 62 | - Manually create a release through https://github.com/sass/libsass-python/releases/ 63 | 64 | Ping Hong Minhee (hongminhee@member.fsf.org, @dahlia on GitHub) if you need 65 | any help! 66 | 67 | .. _PyPI: https://pypi.org/pypi/libsass/ 68 | __ https://sass.github.io/libsass-python/ 69 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | libsass-python: Sass_/SCSS for Python 2 | ===================================== 3 | 4 | LibSass has reached its end of life. It will no longer receive updates of any kind, 5 | nor will its wrapper libraries. Users should migrate to Dart Sass at their earliest 6 | convenience. See also https://github.com/attakei/sass-embedded-python. 7 | 8 | .. image:: https://badge.fury.io/py/libsass.svg 9 | :alt: PyPI 10 | :target: https://pypi.org/pypi/libsass/ 11 | 12 | .. image:: https://github.com/sass/libsass-python/actions/workflows/main.yml/badge.svg 13 | :target: https://github.com/sass/libsass-python/actions/workflows/main.yml 14 | :alt: Build Status 15 | 16 | .. image:: https://results.pre-commit.ci/badge/github/sass/libsass-python/main.svg 17 | :target: https://results.pre-commit.ci/latest/github/sass/libsass-python/main 18 | :alt: pre-commit.ci status 19 | 20 | This package provides a simple Python extension module ``sass`` which is 21 | binding LibSass_ (written in C/C++ by Hampton Catlin and Aaron Leung). 22 | It's very straightforward and there isn't any headache related to Python 23 | distribution/deployment. That means you can add just ``libsass`` into 24 | your ``setup.py``'s ``install_requires`` list or ``requirements.txt`` file. 25 | No need for Ruby nor Node.js. 26 | 27 | .. _Sass: https://sass-lang.com/ 28 | .. _LibSass: https://github.com/sass/libsass 29 | 30 | 31 | Features 32 | -------- 33 | 34 | - You don't need any Ruby/Node.js stack at all, for development or deployment 35 | either. 36 | - Fast. (LibSass_ is written in C++.) 37 | - Simple API. See the below example code for details. 38 | - Custom functions. 39 | - ``@import`` callbacks. 40 | - Support both tabbed (Sass) and braces (SCSS) syntax. 41 | - WSGI middleware for ease of development. 42 | It automatically compiles Sass/SCSS files for each request. 43 | - ``setuptools``/``distutils`` integration. 44 | You can build all Sass/SCSS files using 45 | ``setup.py build_sass`` command. 46 | - Works also on PyPy. 47 | - Provides prebuilt wheel_ binaries for Linux, Windows, and Mac. 48 | 49 | .. _wheel: https://www.python.org/dev/peps/pep-0427/ 50 | 51 | 52 | Install 53 | ------- 54 | 55 | It's available on PyPI_, so you can install it using ``pip`` (or 56 | ``easy_install``): 57 | 58 | .. code-block:: console 59 | 60 | $ pip install libsass 61 | 62 | .. note:: 63 | 64 | libsass requires some features introduced by the recent C++ standard. 65 | You need a C++ compiler that support those features. 66 | See also libsass project's README_ file. 67 | 68 | .. _PyPI: https://pypi.org/pypi/libsass/ 69 | .. _README: https://github.com/sass/libsass#readme 70 | 71 | 72 | .. _example: 73 | 74 | Example 75 | ------- 76 | 77 | .. code-block:: pycon 78 | 79 | >>> import sass 80 | >>> print sass.compile(string='a { b { color: blue; } }') 81 | a b { 82 | color: blue; } 83 | 84 | 85 | Docs 86 | ---- 87 | 88 | There's the user guide manual and the full API reference for ``libsass``: 89 | 90 | https://sass.github.io/libsass-python/ 91 | 92 | You can build the docs by yourself: 93 | 94 | .. code-block:: console 95 | 96 | $ cd docs/ 97 | $ make html 98 | 99 | The built docs will go to ``docs/_build/html/`` directory. 100 | 101 | 102 | Credit 103 | ------ 104 | 105 | Hong Minhee wrote this Python binding of LibSass_. 106 | 107 | Hampton Catlin and Aaron Leung wrote LibSass_, which is portable C/C++ 108 | implementation of Sass_. 109 | 110 | Hampton Catlin originally designed Sass_ language and wrote the first 111 | reference implementation of it in Ruby. 112 | 113 | The above three are all distributed under `MIT license`_. 114 | 115 | .. _MIT license: https://mit-license.org/ 116 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | libsass-python: Sass_/SCSS for Python 2 | ===================================== 3 | 4 | This package provides a simple Python extension module :mod:`sass` which is 5 | binding LibSass_ (written in C/C++ by Hampton Catlin and Aaron Leung). 6 | It's very straightforward and there isn't any headache related to Python 7 | distribution/deployment. That means you can add just ``libsass`` into 8 | your :file:`setup.py`'s ``install_requires`` list or :file:`requirements.txt` 9 | file. 10 | 11 | .. _Sass: https://sass-lang.com/ 12 | .. _LibSass: https://github.com/sass/libsass 13 | 14 | 15 | Features 16 | -------- 17 | 18 | - You don't need any Ruby/Node.js stack at all, for development or deployment 19 | either. 20 | - Fast. (LibSass_ is written in C++.) 21 | - Simple API. See :ref:`example code ` for details. 22 | - Custom functions. 23 | - ``@import`` callbacks. 24 | - Support both tabbed (Sass) and braces (SCSS) syntax. 25 | - WSGI middleware for ease of development. 26 | It automatically compiles Sass/SCSS files for each request. 27 | See also :mod:`sassutils.wsgi` for details. 28 | - :mod:`setuptools`/:mod:`distutils` integration. 29 | You can build all Sass/SCSS files using 30 | :program:`setup.py build_sass` command. 31 | See also :mod:`sassutils.distutils` for details. 32 | - Works also on PyPy. 33 | - Provides prebuilt wheel (:pep:`427`) binaries for Windows and Mac. 34 | 35 | 36 | Install 37 | ------- 38 | 39 | It's available on PyPI_, so you can install it using :program:`pip`: 40 | 41 | .. code-block:: console 42 | 43 | $ pip install libsass 44 | 45 | .. note:: 46 | 47 | libsass requires some features introduced by the recent C++ standard. 48 | You need a C++ compiler that support those features. 49 | See also libsass project's README_ file. 50 | 51 | .. _PyPI: https://pypi.org/pypi/libsass/ 52 | .. _README: https://github.com/sass/libsass#readme 53 | 54 | 55 | .. _example: 56 | 57 | Examples 58 | -------- 59 | 60 | Compile a String of Sass to CSS 61 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 62 | 63 | >>> import sass 64 | >>> sass.compile(string='a { b { color: blue; } }') 65 | 'a b {\n color: blue; }\n' 66 | 67 | Compile a Directory of Sass Files to CSS 68 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 69 | 70 | >>> import sass 71 | >>> import os 72 | >>> os.mkdir('css') 73 | >>> os.mkdir('sass') 74 | >>> scss = """\ 75 | ... $theme_color: #cc0000; 76 | ... body { 77 | ... background-color: $theme_color; 78 | ... } 79 | ... """ 80 | >>> with open('sass/example.scss', 'w') as example_scss: 81 | ... example_scss.write(scss) 82 | ... 83 | >>> sass.compile(dirname=('sass', 'css'), output_style='compressed') 84 | >>> with open('css/example.css') as example_css: 85 | ... print(example_css.read()) 86 | ... 87 | body{background-color:#c00} 88 | 89 | 90 | User's Guide 91 | ------------ 92 | 93 | .. toctree:: 94 | :maxdepth: 2 95 | 96 | frameworks/flask 97 | changes 98 | 99 | 100 | References 101 | ---------- 102 | 103 | .. toctree:: 104 | :maxdepth: 2 105 | 106 | pysassc 107 | sass 108 | sassutils 109 | 110 | 111 | Credit 112 | ------ 113 | 114 | Hong Minhee wrote this Python binding of LibSass_. 115 | 116 | Hampton Catlin and Aaron Leung wrote LibSass_, which is portable C/C++ 117 | implementation of Sass_. 118 | 119 | Hampton Catlin originally designed Sass_ language and wrote the first 120 | reference implementation of it in Ruby. 121 | 122 | The above three are all distributed under `MIT license`_. 123 | 124 | .. _MIT license: https://mit-license.org/ 125 | 126 | 127 | Open source 128 | ----------- 129 | 130 | GitHub (Git repository + issues) 131 | https://github.com/sass/libsass-python 132 | 133 | GitHub Actions (linux + macos + windows) 134 | 135 | .. image:: https://github.com/sass/libsass-python/actions/workflows/main.yml/badge.svg 136 | :target: https://github.com/sass/libsass-python/actions/workflows/main.yml 137 | :alt: Build Status 138 | 139 | PyPI 140 | https://pypi.org/pypi/libsass/ 141 | 142 | .. image:: https://badge.fury.io/py/libsass.svg 143 | :alt: PyPI 144 | :target: https://pypi.org/pypi/libsass/ 145 | 146 | Changelog 147 | :doc:`changes` 148 | 149 | 150 | Indices and tables 151 | ------------------ 152 | 153 | - :ref:`genindex` 154 | - :ref:`modindex` 155 | - :ref:`search` 156 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\libsass.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\libsass.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/frameworks/flask.rst: -------------------------------------------------------------------------------- 1 | Using with Flask 2 | ================ 3 | 4 | This guide explains how to use libsass with the Flask_ web framework. 5 | :mod:`sassutils` package provides several tools that can be integrated 6 | into web applications written in Flask. 7 | 8 | .. _Flask: http://flask.pocoo.org/ 9 | 10 | .. contents:: 11 | 12 | 13 | Directory layout 14 | ---------------- 15 | 16 | Imagine the project contained in such directory layout: 17 | 18 | - :file:`setup.py` 19 | - :file:`myapp/` 20 | 21 | - :file:`__init__.py` 22 | - :file:`static/` 23 | 24 | - :file:`sass/` 25 | - :file:`css/` 26 | - :file:`templates/` 27 | 28 | Sass/SCSS files will go inside :file:`myapp/static/sass/` directory. 29 | Compiled CSS files will go inside :file:`myapp/static/css/` directory. 30 | CSS files can be regenerated, so add :file:`myapp/static/css/` into your 31 | ignore list like :file:`.gitignore` or :file:`.hgignore`. 32 | 33 | 34 | Defining manifest 35 | ----------------- 36 | 37 | The :mod:`sassutils` defines a concept named :dfn:`manifest`. 38 | Manifest is the build settings of Sass/SCSS. It specifies some paths 39 | related to building Sass/SCSS: 40 | 41 | - The path of the directory which contains Sass/SCSS source files. 42 | - The path of the directory which the compiled CSS files will go. 43 | - The path, exposed to HTTP (through WSGI), of the directory that 44 | will contain the compiled CSS files. 45 | 46 | Every package may have its own manifest. Paths have to be relative 47 | to the path of the package. 48 | 49 | For example, in the above project, the package name is :mod:`myapp`. 50 | The path of the package is :file:`myapp/`. The path of the Sass/SCSS 51 | directory is :file:`static/sass/` (relative to the package directory). 52 | The path of the CSS directory is :file:`static/css/`. 53 | The exposed path is :file:`/static/css`. 54 | 55 | These settings can be represented as the following manifests:: 56 | 57 | { 58 | 'myapp': ('static/sass', 'static/css', '/static/css') 59 | } 60 | 61 | As you can see the above, the set of manifests are represented in dictionary, 62 | in which the keys are packages names and the values are tuples of paths. 63 | 64 | 65 | Building Sass/SCSS for each request 66 | ----------------------------------- 67 | 68 | .. seealso:: 69 | 70 | Flask --- `Hooking in WSGI Middlewares`__ 71 | The section which explains how to integrate WSGI middlewares to 72 | Flask. 73 | 74 | Flask --- :ref:`flask:app-dispatch` 75 | The documentation which explains how Flask dispatches each 76 | request internally. 77 | 78 | __ http://flask.pocoo.org/docs/quickstart/#hooking-in-wsgi-middlewares 79 | 80 | In development, manually building Sass/SCSS files for each change is 81 | a tedious task. :class:`~sassutils.wsgi.SassMiddleware` makes the web 82 | application build Sass/SCSS files for each request automatically. 83 | It's a WSGI middleware, so it can be plugged into the web app written in 84 | Flask. 85 | 86 | :class:`~sassutils.wsgi.SassMiddleware` takes two required parameters: 87 | 88 | - The WSGI-compliant callable object. 89 | - The set of manifests represented as a dictionary. 90 | 91 | So:: 92 | 93 | from flask import Flask 94 | from sassutils.wsgi import SassMiddleware 95 | 96 | app = Flask(__name__) 97 | 98 | app.wsgi_app = SassMiddleware(app.wsgi_app, { 99 | 'myapp': ('static/sass', 'static/css', '/static/css') 100 | }) 101 | 102 | And then, if you want to link a compiled CSS file, use the 103 | :func:`~flask.url_for()` function: 104 | 105 | .. sourcecode:: html+jinja 106 | 107 | 109 | 110 | .. note:: 111 | 112 | The linked filename is :file:`style.scss.css`, not just :file:`style.scss`. 113 | All compiled filenames have trailing ``.css`` suffix. 114 | 115 | 116 | Building Sass/SCSS for each deployment 117 | -------------------------------------- 118 | 119 | .. note:: 120 | 121 | This section assumes that you use setuptools_ for deployment. 122 | 123 | .. seealso:: 124 | 125 | Flask --- :ref:`flask:distribute-deployment` 126 | How to deploy Flask application using setuptools_. 127 | 128 | If libsass is installed in the :file:`site-packages` (for example, 129 | your virtualenv), the :file:`setup.py` script also gets a new command 130 | provided by libsass: :class:`~sassutils.distutils.build_sass`. 131 | The command is aware of the ``sass_manifests`` option of :file:`setup.py` and 132 | builds all Sass/SCSS sources according to the manifests. 133 | 134 | Add these arguments to :file:`setup.py` script:: 135 | 136 | setup( 137 | # ..., 138 | setup_requires=['libsass >= 0.6.0'], 139 | sass_manifests={ 140 | 'myapp': ('static/sass', 'static/css', '/static/css') 141 | } 142 | ) 143 | 144 | The ``setup_requires`` option makes sure that libsass is installed 145 | in :file:`site-packages` (for example, your virtualenv) before 146 | the :file:`setup.py` script. That means if you run the :file:`setup.py` 147 | script and libsass isn't installed in advance, it will automatically 148 | install libsass first. 149 | 150 | The ``sass_manifests`` specifies the manifests for libsass. 151 | 152 | Now :program:`setup.py build_sass` will compile all Sass/SCSS files 153 | in the specified path and generates compiled CSS files inside the specified 154 | path (according to the manifests). 155 | 156 | If you use it with ``sdist`` or ``bdist`` commands, the packed archive will 157 | also contain the compiled CSS files! 158 | 159 | .. sourcecode:: console 160 | 161 | $ python setup.py build_sass sdist 162 | 163 | You can add aliases to make these commands always run the ``build_sass`` 164 | command first. Make :file:`setup.cfg` config: 165 | 166 | .. sourcecode:: ini 167 | 168 | [aliases] 169 | sdist = build_sass sdist 170 | bdist = build_sass bdist 171 | 172 | Now it automatically builds Sass/SCSS sources and include the compiled CSS files 173 | to the package archive when you run :program:`setup.py sdist`. 174 | 175 | .. _setuptools: https://pypi.org/pypi/setuptools/ 176 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/libsass.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/libsass.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/libsass" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/libsass" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /sassutils/wsgi.py: -------------------------------------------------------------------------------- 1 | """:mod:`sassutils.wsgi` --- WSGI middleware for development purpose 2 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | 4 | """ 5 | import collections.abc 6 | import logging 7 | import os.path 8 | 9 | from pkg_resources import resource_filename 10 | 11 | from .builder import Manifest 12 | from sass import CompileError 13 | 14 | __all__ = 'SassMiddleware', 15 | 16 | 17 | class SassMiddleware: 18 | r"""WSGI middleware for development purpose. Every time a CSS file has 19 | requested it finds a matched Sass/SCSS source file and then compiled 20 | it into CSS. 21 | 22 | It shows syntax errors in three ways: 23 | 24 | Heading comment 25 | The result CSS includes detailed error message in the heading 26 | CSS comment e.g.: 27 | 28 | .. code-block:: css 29 | 30 | /* 31 | Error: invalid property name 32 | */ 33 | 34 | Red text in ``body:before`` 35 | The result CSS draws detailed error message in ``:before`` 36 | pseudo-class of ``body`` element e.g.: 37 | 38 | .. code-block:: css 39 | 40 | body:before { 41 | content: 'Error: invalid property name'; 42 | color: maroon; 43 | background-color: white; 44 | } 45 | 46 | In most cases you could be aware of syntax error by refreshing your 47 | working document because it will removes all other styles and leaves 48 | only a red text. 49 | 50 | :mod:`logging` 51 | It logs syntax errors if exist during compilation to 52 | ``sassutils.wsgi.SassMiddleware`` logger with level ``ERROR``. 53 | 54 | To enable this:: 55 | 56 | from logging import Formatter, StreamHandler, getLogger 57 | logger = getLogger('sassutils.wsgi.SassMiddleware') 58 | handler = StreamHandler(level=logging.ERROR) 59 | formatter = Formatter(fmt='*' * 80 + '\n%(message)s\n' + '*' * 80) 60 | handler.setFormatter(formatter) 61 | logger.addHandler(handler) 62 | 63 | Or simply:: 64 | 65 | import logging 66 | logging.basicConfig() 67 | 68 | :param app: the WSGI application to wrap 69 | :type app: :class:`collections.abc.Callable` 70 | :param manifests: build settings. the same format to 71 | :file:`setup.py` script's ``sass_manifests`` 72 | option 73 | :type manifests: :class:`collections.abc.Mapping` 74 | :param package_dir: optional mapping of package names to directories. 75 | the same format to :file:`setup.py` script's 76 | ``package_dir`` option 77 | :type package_dir: :class:`collections.abc.Mapping` 78 | 79 | .. versionchanged:: 0.4.0 80 | It creates also source map files with filenames followed by 81 | :file:`.map` suffix. 82 | 83 | .. versionadded:: 0.8.0 84 | It logs syntax errors if exist during compilation to 85 | ``sassutils.wsgi.SassMiddleware`` logger with level ``ERROR``. 86 | 87 | """ 88 | 89 | def __init__( 90 | self, app, manifests, package_dir={}, 91 | error_status='200 OK', 92 | ): 93 | if not callable(app): 94 | raise TypeError( 95 | 'app must be a WSGI-compliant callable object, ' 96 | 'not ' + repr(app), 97 | ) 98 | self.app = app 99 | self.manifests = Manifest.normalize_manifests(manifests) 100 | if not isinstance(package_dir, collections.abc.Mapping): 101 | raise TypeError( 102 | 'package_dir must be a mapping object, not ' + 103 | repr(package_dir), 104 | ) 105 | self.error_status = error_status 106 | self.package_dir = dict(package_dir) 107 | for package_name in self.manifests: 108 | if package_name in self.package_dir: 109 | continue 110 | path = resource_filename(package_name, '') 111 | self.package_dir[package_name] = path 112 | self.paths = [] 113 | for package_name, manifest in self.manifests.items(): 114 | wsgi_path = manifest.wsgi_path 115 | if not wsgi_path.startswith('/'): 116 | wsgi_path = '/' + wsgi_path 117 | if not wsgi_path.endswith('/'): 118 | wsgi_path += '/' 119 | package_dir = self.package_dir[package_name] 120 | self.paths.append((wsgi_path, package_dir, manifest)) 121 | 122 | def __call__(self, environ, start_response): 123 | path = environ.get('PATH_INFO', '/') 124 | if path.endswith('.css'): 125 | for prefix, package_dir, manifest in self.paths: 126 | if not path.startswith(prefix): 127 | continue 128 | css_filename = path[len(prefix):] 129 | sass_filename = manifest.unresolve_filename( 130 | package_dir, css_filename, 131 | ) 132 | try: 133 | result = manifest.build_one( 134 | package_dir, 135 | sass_filename, 136 | source_map=True, 137 | ) 138 | except OSError: 139 | break 140 | except CompileError as e: 141 | logger = logging.getLogger(__name__ + '.SassMiddleware') 142 | logger.error(str(e)) 143 | start_response( 144 | self.error_status, 145 | [('Content-Type', 'text/css; charset=utf-8')], 146 | ) 147 | return [ 148 | b'/*\n', str(e).encode('utf-8'), b'\n*/\n\n', 149 | b'body:before { content: ', 150 | self.quote_css_string(str(e)).encode('utf-8'), 151 | b'; color: maroon; background-color: white', 152 | b'; white-space: pre-wrap; display: block', 153 | b'; font-family: "Courier New", monospace' 154 | b'; user-select: text; }', 155 | ] 156 | 157 | def read_file(path): 158 | with open(path, 'rb') as in_: 159 | while 1: 160 | chunk = in_.read(4096) 161 | if chunk: 162 | yield chunk 163 | else: 164 | break 165 | start_response('200 OK', [('Content-Type', 'text/css')]) 166 | return read_file(os.path.join(package_dir, result)) 167 | return self.app(environ, start_response) 168 | 169 | @staticmethod 170 | def quote_css_string(s): 171 | """Quotes a string as CSS string literal.""" 172 | return "'" + ''.join('\\%06x' % ord(c) for c in s) + "'" 173 | -------------------------------------------------------------------------------- /sassutils/distutils.py: -------------------------------------------------------------------------------- 1 | """:mod:`sassutils.distutils` --- :mod:`setuptools`/:mod:`distutils` integrat\ 2 | ion 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\ 4 | ~~~ 5 | 6 | This module provides extensions (and some magical monkey-patches, sorry) 7 | of the standard :mod:`distutils` and :mod:`setuptools` (now it's named 8 | Distribute) for libsass. 9 | 10 | To use this, add ``libsass`` into ``setup_requires`` (not ``install_requires``) 11 | option of the :file:`setup.py` script:: 12 | 13 | from setuptools import setup 14 | 15 | setup( 16 | # ..., 17 | setup_requires=['libsass >= 0.6.0'] 18 | ) 19 | 20 | It will adds :class:`build_sass` command to the :file:`setup.py` script: 21 | 22 | .. sourcecode:: console 23 | 24 | $ python setup.py build_sass 25 | 26 | This commands builds Sass/SCSS files to compiled CSS files of the project 27 | and makes the package archive (made by :class:`~distutils.command.sdist.sdist`, 28 | :class:`~distutils.command.bdist.bdist`, and so on) to include these compiled 29 | CSS files. 30 | 31 | To set the directory of Sass/SCSS source files and the directory to 32 | store compiled CSS files, specify ``sass_manifests`` option:: 33 | 34 | from setuptools import find_packages, setup 35 | 36 | setup( 37 | name='YourPackage', 38 | packages=find_packages(), 39 | sass_manifests={ 40 | 'your.webapp': ('static/sass', 'static/css') 41 | }, 42 | setup_requires=['libsass >= 0.6.0'] 43 | ) 44 | 45 | The option should be a mapping of package names to pairs of paths, e.g.:: 46 | 47 | { 48 | 'package': ('static/sass', 'static/css'), 49 | 'package.name': ('static/scss', 'static') 50 | } 51 | 52 | The option can also be a mapping of package names to manifest dictionaries:: 53 | 54 | { 55 | 'package': { 56 | 'sass_path': 'static/sass', 57 | 'css_path': 'static/css', 58 | 'strip_extension': True, 59 | }, 60 | } 61 | 62 | .. versionadded:: 0.15.0 63 | Added ``strip_extension`` so ``a.scss`` is compiled to ``a.css`` instead 64 | of ``a.scss.css``. This option will default to ``True`` in the future. 65 | 66 | .. versionadded:: 0.6.0 67 | Added ``--output-style``/``-s`` option to :class:`build_sass` command. 68 | 69 | """ 70 | import functools 71 | import os.path 72 | 73 | import distutils.errors 74 | import distutils.log 75 | import distutils.util 76 | from setuptools import Command 77 | from setuptools.command.sdist import sdist 78 | 79 | from .builder import Manifest 80 | from sass import OUTPUT_STYLES 81 | 82 | __all__ = 'build_sass', 'validate_manifests' 83 | 84 | 85 | def validate_manifests(dist, attr, value): 86 | """Verifies that ``value`` is an expected mapping of package to 87 | :class:`sassutils.builder.Manifest`. 88 | 89 | """ 90 | try: 91 | Manifest.normalize_manifests(value) 92 | except TypeError: 93 | raise distutils.errors.DistutilsSetupError( 94 | attr + "must be a mapping object like: {'package.name': " 95 | "sassutils.distutils.Manifest('sass/path')}, or as shorten form: " 96 | "{'package.name': ('sass/path', 'css/path'}), not " + 97 | repr(value), 98 | ) 99 | 100 | 101 | class build_sass(Command): 102 | """Builds Sass/SCSS files to CSS files.""" 103 | 104 | description = __doc__ 105 | user_options = [ 106 | ( 107 | 'output-style=', 's', 108 | 'Coding style of the compiled result. Choose one of ' + 109 | ', '.join(OUTPUT_STYLES), 110 | ), 111 | ] 112 | 113 | def initialize_options(self): 114 | self.package_dir = None 115 | self.output_style = 'nested' 116 | 117 | def finalize_options(self): 118 | self.package_dir = {} 119 | if self.distribution.package_dir: 120 | self.package_dir = {} 121 | for name, path in self.distribution.package_dir.items(): 122 | self.package_dir[name] = distutils.util.convert_path(path) 123 | 124 | def run(self): 125 | manifests = self.distribution.sass_manifests 126 | manifests = Manifest.normalize_manifests(manifests) 127 | self.distribution.sass_manifests = manifests 128 | package_data = self.distribution.package_data 129 | data_files = self.distribution.data_files or [] 130 | for package_name, manifest in manifests.items(): 131 | package_dir = self.get_package_dir(package_name) 132 | distutils.log.info("building '%s' sass", package_name) 133 | css_files = manifest.build( 134 | package_dir, 135 | output_style=self.output_style, 136 | ) 137 | map(distutils.log.info, css_files) 138 | package_data.setdefault(package_name, []).extend(css_files) 139 | data_files.append( 140 | ( 141 | package_dir, 142 | [os.path.join(package_dir, f) for f in css_files], 143 | ), 144 | ) 145 | self.distribution.package_data = package_data 146 | self.distribution.data_files = data_files 147 | self.distribution.has_data_files = lambda: True 148 | # See the below monkey patch (end of this source code). 149 | self.distribution.compiled_sass_files = data_files 150 | 151 | def get_package_dir(self, package): 152 | """Returns the directory, relative to the top of the source 153 | distribution, where package ``package`` should be found 154 | (at least according to the :attr:`package_dir` option, if any). 155 | 156 | Copied from :meth:`distutils.command.build_py.get_package_dir()` 157 | method. 158 | 159 | """ 160 | path = package.split('.') 161 | if not self.package_dir: 162 | if path: 163 | return os.path.join(*path) 164 | return '' 165 | tail = [] 166 | while path: 167 | try: 168 | pdir = self.package_dir['.'.join(path)] 169 | except KeyError: 170 | tail.insert(0, path[-1]) 171 | del path[-1] 172 | else: 173 | tail.insert(0, pdir) 174 | return os.path.join(*tail) 175 | else: 176 | pdir = self.package_dir.get('') 177 | if pdir is not None: 178 | tail.insert(0, pdir) 179 | if tail: 180 | return os.path.join(*tail) 181 | return '' 182 | 183 | 184 | # Does monkey-patching the setuptools.command.sdist.sdist.check_readme() 185 | # method to include compiled Sass files as data files. 186 | if not hasattr(sdist, '_wrapped_check_readme'): 187 | @functools.wraps(sdist.check_readme) 188 | def check_readme(self): 189 | try: 190 | files = self.distribution.compiled_sass_files 191 | except AttributeError: 192 | pass 193 | else: 194 | for _, css_files in files: 195 | self.filelist.extend(css_files) 196 | return self._wrapped_check_readme() 197 | sdist._wrapped_check_readme = sdist.check_readme 198 | sdist.check_readme = check_readme 199 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | import os.path 3 | import platform 4 | import shutil 5 | import subprocess 6 | import sys 7 | import tempfile 8 | 9 | import distutils.cmd 10 | import distutils.log 11 | import distutils.sysconfig 12 | from setuptools import Extension 13 | from setuptools import setup 14 | 15 | MACOS_FLAG = ['-mmacosx-version-min=10.7'] 16 | FLAGS_POSIX = [ 17 | '-fPIC', '-std=gnu++0x', '-Wall', '-Wno-parentheses', '-Werror=switch', 18 | ] 19 | FLAGS_CLANG = ['-c', '-O3'] + FLAGS_POSIX + ['-stdlib=libc++'] 20 | LFLAGS_POSIX = ['-fPIC', '-lstdc++'] 21 | LFLAGS_CLANG = ['-fPIC', '-stdlib=libc++'] 22 | 23 | sources = ['_sass.c'] 24 | headers = [] 25 | 26 | if sys.platform == 'win32': 27 | extra_compile_args = ['/Od', '/EHsc', '/MT'] 28 | extra_link_args = [] 29 | elif platform.system() == 'Darwin': 30 | extra_compile_args = FLAGS_CLANG + MACOS_FLAG 31 | extra_link_args = LFLAGS_CLANG + MACOS_FLAG 32 | elif platform.system() in {'FreeBSD', 'OpenBSD'}: 33 | extra_compile_args = FLAGS_CLANG 34 | extra_link_args = LFLAGS_CLANG 35 | else: 36 | extra_compile_args = FLAGS_POSIX 37 | extra_link_args = LFLAGS_POSIX 38 | 39 | if platform.system() in {'Darwin', 'FreeBSD', 'OpenBSD'}: 40 | os.environ.setdefault('CC', 'clang') 41 | os.environ.setdefault('CXX', 'clang++') 42 | orig_customize_compiler = distutils.sysconfig.customize_compiler 43 | 44 | def customize_compiler(compiler): 45 | orig_customize_compiler(compiler) 46 | compiler.compiler[0] = os.environ['CC'] 47 | compiler.compiler_so[0] = os.environ['CXX'] 48 | compiler.compiler_cxx[0] = os.environ['CXX'] 49 | compiler.linker_so[0] = os.environ['CXX'] 50 | return compiler 51 | distutils.sysconfig.customize_compiler = customize_compiler 52 | 53 | if os.environ.get('SYSTEM_SASS', False): 54 | libraries = ['sass'] 55 | include_dirs = [] 56 | else: 57 | LIBSASS_SOURCE_DIR = os.path.join('libsass', 'src') 58 | 59 | if ( 60 | not os.path.isfile(os.path.join('libsass', 'Makefile')) and 61 | os.path.isdir('.git') 62 | ): 63 | print(file=sys.stderr) 64 | print('Missing the libsass sumbodule. Try:', file=sys.stderr) 65 | print(' git submodule update --init', file=sys.stderr) 66 | print(file=sys.stderr) 67 | exit(1) 68 | 69 | # Determine the libsass version from the git checkout 70 | if os.path.exists(os.path.join('libsass', '.git')): 71 | out = subprocess.check_output(( 72 | 'git', '-C', 'libsass', 'describe', 73 | '--abbrev=4', '--dirty', '--always', '--tags', 74 | )) 75 | with open('.libsass-upstream-version', 'wb') as libsass_version_file: 76 | libsass_version_file.write(out) 77 | 78 | # The version file should always exist at this point 79 | with open('.libsass-upstream-version', 'rb') as libsass_version_file: 80 | libsass_version = libsass_version_file.read().decode('UTF-8').strip() 81 | if sys.platform == 'win32': 82 | # This looks wrong, but is required for some reason :( 83 | define = fr'/DLIBSASS_VERSION="\"{libsass_version}\""' 84 | else: 85 | define = f'-DLIBSASS_VERSION="{libsass_version}"' 86 | 87 | for directory in ( 88 | os.path.join('libsass', 'src'), 89 | os.path.join('libsass', 'include'), 90 | ): 91 | for pth, _, filenames in os.walk(directory): 92 | for filename in filenames: 93 | filename = os.path.join(pth, filename) 94 | if filename.endswith(('.c', '.cpp')): 95 | sources.append(filename) 96 | elif filename.endswith('.h'): 97 | headers.append(filename) 98 | 99 | if platform.system() in {'Darwin', 'FreeBSD', 'OpenBSD'}: 100 | # Dirty workaround to avoid link error... 101 | # Python distutils doesn't provide any way 102 | # to configure different flags for each cc and c++. 103 | cencode_path = os.path.join(LIBSASS_SOURCE_DIR, 'cencode.c') 104 | cencode_body = '' 105 | with open(cencode_path) as f: 106 | cencode_body = f.read() 107 | with open(cencode_path, 'w') as f: 108 | f.write( 109 | '#ifdef __cplusplus\n' 110 | 'extern "C" {\n' 111 | '#endif\n', 112 | ) 113 | f.write(cencode_body) 114 | f.write( 115 | '#ifdef __cplusplus\n' 116 | '}\n' 117 | '#endif\n', 118 | ) 119 | 120 | @atexit.register 121 | def restore_cencode(): 122 | if os.path.isfile(cencode_path): 123 | with open(cencode_path, 'w') as f: 124 | f.write(cencode_body) 125 | 126 | libraries = [] 127 | include_dirs = [os.path.join('.', 'libsass', 'include')] 128 | extra_compile_args.append(define) 129 | 130 | # Py_LIMITED_API does not work for pypy 131 | # https://foss.heptapod.net/pypy/pypy/issues/3173 132 | if not hasattr(sys, 'pypy_version_info'): 133 | py_limited_api = True 134 | define_macros = [('Py_LIMITED_API', None)] 135 | else: 136 | py_limited_api = False 137 | define_macros = [] 138 | 139 | sass_extension = Extension( 140 | '_sass', 141 | sorted(sources), 142 | include_dirs=include_dirs, 143 | depends=headers, 144 | extra_compile_args=extra_compile_args, 145 | extra_link_args=extra_link_args, 146 | libraries=libraries, 147 | py_limited_api=py_limited_api, 148 | define_macros=define_macros, 149 | ) 150 | 151 | 152 | class upload_doc(distutils.cmd.Command): 153 | """Uploads the documentation to GitHub pages.""" 154 | 155 | description = __doc__ 156 | user_options = [] 157 | 158 | def initialize_options(self): 159 | if sys.version_info < (3,): 160 | raise SystemExit('upload_doc must be run with python 3') 161 | 162 | def finalize_options(self): 163 | pass 164 | 165 | def run(self): 166 | path = tempfile.mkdtemp() 167 | build = os.path.join( 168 | os.path.dirname(os.path.abspath(__file__)), 169 | 'build', 'sphinx', 'html', 170 | ) 171 | os.chdir(path) 172 | os.system( 173 | 'git clone -b gh-pages --depth 5 ' 174 | 'git@github.com:sass/libsass-python.git .', 175 | ) 176 | os.system('git rm -r .') 177 | os.system('touch .nojekyll') 178 | os.system('cp -r ' + build + '/* .') 179 | os.system('git stage .') 180 | os.system('git commit -a -m "Documentation updated."') 181 | os.system('git push origin gh-pages') 182 | shutil.rmtree(path) 183 | 184 | 185 | cmdclass = {'upload_doc': upload_doc} 186 | 187 | if sys.version_info >= (3,) and platform.python_implementation() == 'CPython': 188 | try: 189 | import wheel.bdist_wheel 190 | except ImportError: 191 | pass 192 | else: 193 | class bdist_wheel(wheel.bdist_wheel.bdist_wheel): 194 | def finalize_options(self): 195 | self.py_limited_api = f'cp3{sys.version_info[1]}' 196 | super().finalize_options() 197 | 198 | cmdclass['bdist_wheel'] = bdist_wheel 199 | 200 | 201 | setup( 202 | ext_modules=[sass_extension], 203 | cmdclass=cmdclass, 204 | ) 205 | -------------------------------------------------------------------------------- /pysassc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | r""":mod:`pysassc` --- SassC compliant command line interface 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | This provides SassC_ compliant CLI executable named :program:`pysassc`: 6 | 7 | .. sourcecode:: console 8 | 9 | $ pysassc 10 | Usage: pysassc [options] SCSS_FILE [CSS_FILE] 11 | 12 | There are options as well: 13 | 14 | .. option:: -t