diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f58e6a0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,29 @@ +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 +end_of_line = lf +max_line_length = 88 + +[*.{json,yml,yaml,js,jsx,vue,toml}] +indent_size = 2 + +[*.{html,htm,svg,xml}] +indent_size = 2 +max_line_length = 120 + +[*.{css,scss}] +indent_size = 2 + +[LICENSE] +insert_final_newline = false + +[*.{md,markdown}] +indent_size = 2 +max_line_length = 80 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b6440f7 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: + - codingjoe diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..7df6849 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: weekly +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..922705c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +jobs: + + dist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + - run: python -m pip install --upgrade pip build wheel twine + - run: python -m build --sdist --wheel + - run: python -m twine check dist/* + - uses: actions/upload-artifact@v3 + with: + path: dist/* + + lint: + runs-on: ubuntu-latest + strategy: + matrix: + lint-command: + - bandit -r . -x ./tests + - black --check --diff . + - flake8 . + - isort --check-only --diff . + - pydocstyle . + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + cache: 'pip' + cache-dependency-path: 'linter-requirements.txt' + - run: python -m pip install -r linter-requirements.txt + - run: ${{ matrix.lint-command }} + + pytest: + needs: + - lint + strategy: + matrix: + python-version: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - run: python -m pip install .[test] + - run: python -m pytest + - uses: codecov/codecov-action@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..57fb04f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,21 @@ +name: Release + +on: + release: + types: [published] + +jobs: + + PyPi: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + - run: python -m pip install --upgrade pip build wheel twine + - run: python -m build --sdist --wheel + - run: python -m twine upload dist/* + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} diff --git a/.gitignore b/.gitignore index 68bc17f..1fd15d0 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,7 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + + +# flit-scm +_version.py diff --git a/README.md b/README.md index f57c77e..9800dc1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,37 @@ -# flit-sass +# Flit Sass + +[![PyPi Version](https://img.shields.io/pypi/v/flit-sass.svg)](https://pypi.python.org/pypi/flit-sass/) +[![Test Coverage](https://codecov.io/gh/codingjoe/flit-sass/branch/main/graph/badge.svg)](https://codecov.io/gh/codingjoe/flit-sass) +[![GitHub License](https://img.shields.io/github/license/codingjoe/flit-sass)](https://raw.githubusercontent.com/codingjoe/flit-sass/main/LICENSE) + Compile Sass/SCSS files to CSS during build time. + +_"Binary files should never be committed to a repository to promote transparency and security."_ +That is why this project was created. + +### Usage + +Simple, just add `flit-sass` to your `pyproject.toml` +build-system requirements and set the `build-backend`: + +```toml +# pyproject.toml +[build-system] +requires = [ + "flit-sass[scm]", # [scm] is optional + # …others, like wheel, etc. +] +# using flit-core as a base build-backend +build-backend = "flit_sass.core" +# or using flit-scm as a base build-backend for git-based versioning +build-backend = "flit_sass.scm" +# To use use flit-scm, you will need to include the optional dependency above. + +[tool.flit_sass] +dirs = { "package/static/scss" = "package/static/css" } +output_style = "compressed" # optional: nested, expanded, compact, compressed +``` + +If you ship wheels, those will include the compiled `.css` files. + +**That's it!** diff --git a/flit_sass/__init__.py b/flit_sass/__init__.py new file mode 100644 index 0000000..8831aa0 --- /dev/null +++ b/flit_sass/__init__.py @@ -0,0 +1 @@ +"""Compile Sass/SCSS files to CSS during build time.""" diff --git a/flit_sass/core.py b/flit_sass/core.py new file mode 100644 index 0000000..826a117 --- /dev/null +++ b/flit_sass/core.py @@ -0,0 +1,14 @@ +from flit_core import buildapi +from flit_core.buildapi import * # noqa + +from flit_sass.utils import compile_sass + + +def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): + compile_sass() + return buildapi.build_wheel(wheel_directory, config_settings, metadata_directory) + + +def build_editable(wheel_directory, config_settings=None, metadata_directory=None): + compile_sass() + return buildapi.build_editable(wheel_directory, config_settings, metadata_directory) diff --git a/flit_sass/scm.py b/flit_sass/scm.py new file mode 100644 index 0000000..b87ba53 --- /dev/null +++ b/flit_sass/scm.py @@ -0,0 +1,3 @@ +from flit_scm import * # noqa + +from .core import * # noqa diff --git a/flit_sass/utils.py b/flit_sass/utils.py new file mode 100644 index 0000000..ec83807 --- /dev/null +++ b/flit_sass/utils.py @@ -0,0 +1,27 @@ +try: + import tomllib +except ImportError: + import tomli as tomllib + +import sass + + +def compile_sass(): + """Compile Sass/SCSS files.""" + try: + with open("pyproject.toml", "rb") as f: + pyproject = tomllib.load(f) + except OSError: + pass # Do nothing if unable to access `pyproject.toml` + else: + try: + config = pyproject["tool"]["flit-sass"] + except KeyError: + pass # Do nothing if `setuptools_scm` is not configured in `pyproject.toml` + else: + print("\33[1m* Compiling Sass/SCSS files...\33[0m") + for dirname in config.get("dirs", {}).items(): + sass.compile( + dirname=dirname, + output_style=config.get("output_style", "compressed"), + ) diff --git a/linter-requirements.txt b/linter-requirements.txt new file mode 100644 index 0000000..d132dc9 --- /dev/null +++ b/linter-requirements.txt @@ -0,0 +1,5 @@ +bandit==1.7.5 +black==23.9.1 +flake8==6.1.0 +isort==5.12.0 +pydocstyle[toml]==6.3.0 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..be47453 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,81 @@ +[build-system] +requires = ["flit-scm", "wheel"] +build-backend = "flit_scm:buildapi" + +[project] +name = "flit-sass" +authors = [ + { name = "Johannes Maron", email = "johannes@maron.family" }, +] +readme = "README.md" +license = { file = "LICENSE" } +keywords = ["flit", "pep518", "build", "packaging", "sass"] +dynamic = ["version", "description"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python", + "Environment :: Web Environment", + "License :: OSI Approved :: BSD License", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries :: Python Modules", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +requires-python = ">=3.8" +dependencies = [ + "flit-core~=3.5", + "libsass~=0.21", + "tomli; python_version < '3.11'", +] + +[project.optional-dependencies] +test = [ + "pytest", + "pytest-cov", + "flit-scm", + "build", + "wheel", +] +scm = [ + "flit-scm", +] + +[project.urls] +Project-URL = "https://github.com/codingjoe/flit-sass" +Changelog = "https://github.com/codingjoe/flit-sass/releases" + +[tool.flit.module] +name = "flit_sass" + +[tool.setuptools_scm] +write_to = "flit_sass/_version.py" + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "--cov --tb=short -rxs" +testpaths = ["tests"] + +[tool.coverage.run] +source = ["flit_sass"] +omit = ["flit_sass/_version.py"] + +[tool.coverage.report] +show_missing = true + +[tool.isort] +atomic = true +line_length = 88 +known_first_party = "flit_sass, tests" +include_trailing_comma = true +default_section = "THIRDPARTY" +combine_as_imports = true + +[tool.pydocstyle] +add_ignore = "D1" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..6f60592 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[flake8] +max-line-length=88 +select = C,E,F,W,B,B950 +ignore = E203, E501, W503, E731 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..848737c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,68 @@ +import contextlib +import os +import tempfile +from pathlib import Path + +import pytest + + +@pytest.fixture() +def package(): + with tempfile.TemporaryDirectory() as tmpdirname: + temp_dir = Path(tmpdirname) + pyproject_toml = temp_dir / "pyproject.toml" + pyproject_toml.write_text( + "[project]\n" + 'name = "package"\n' + 'version = "0.1.0"\n' + 'description = "A package"\n' + "[build-system]\n" + 'requires = ["flit_core", "libsass"]\n' + 'build-backend = "flit_sass.core"\n' + "[tool.flit-sass]\n" + 'dirs = { "package/static/sass" = "package/static/css" }\n' + ) + (temp_dir / "package").mkdir(parents=True) + (temp_dir / "package" / "__init__.py").touch() + + with chdir(temp_dir): + yield temp_dir + + +@pytest.fixture() +def package_scm(): + with tempfile.TemporaryDirectory() as tmpdirname: + temp_dir = Path(tmpdirname) + pyproject_toml = temp_dir / "pyproject.toml" + pyproject_toml.write_text( + "[project]\n" + 'name = "package"\n' + 'description = "A package"\n' + 'dynamic = ["version"]\n' + "[build-system]\n" + 'requires = ["flit_core", "libsass", "flit_scm"]\n' + 'build-backend = "flit_sass.scm"\n' + "[tool.setuptools_scm]\n" + 'write_to = "package/_version.py"\n' + 'fallback_version = "0.1.0"\n' + "[tool.flit-sass]\n" + 'dirs = { "package/static/sass" = "package/static/css" }\n' + 'output_style = "nested"\n' + ) + (temp_dir / "package").mkdir(parents=True) + (temp_dir / "package" / "__init__.py").touch() + (temp_dir / "package" / "_version.py").write_text('version = "0.1.0"\n') + + with chdir(temp_dir): + yield temp_dir + + +@contextlib.contextmanager +def chdir(path): + """Context manager to temporarily change the working directory.""" + cwd = Path.cwd() + os.chdir(path) + try: + yield + finally: + os.chdir(cwd) diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..fa5fb45 --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,92 @@ +import os +import subprocess # nosec +import sys +from pathlib import Path + +ROOT = Path(__file__).parent.parent + + +def test_build_sdist(package): + """Test that build_sdist() compiles Sass files.""" + # Create a dummy SCSS file + scss_file = package / "package/static/sass/styles.scss" + scss_file.parent.mkdir(parents=True) + scss_file.write_text( + r""" + $color: #abc; + body { + color: $color; + } + """ + ) + + output = subprocess.check_output( + [sys.executable, "-m", "build", "--sdist"], + cwd=package, + env={**os.environ, "PYTHONPATH": f".:{ROOT}"}, + ) + + # Check that the Sass compilation was logged + assert b"* Compiling Sass/CSSS files..." not in output + + +def test_build_wheel(package): + """Test that build_wheel() compiles Sass files.""" + # Create a dummy SCSS file + scss_file = package / "package/static/sass/styles.scss" + scss_file.parent.mkdir(parents=True) + scss_file.write_text( + r""" + $color: #abc; + body { + color: $color; + } + """ + ) + + output = subprocess.check_output( + [sys.executable, "-m", "build", "--wheel"], + cwd=package, + env={**os.environ, "PYTHONPATH": f".:{ROOT}"}, + ) + + # Check that the Sass compilation was logged + assert b"* Compiling Sass/SCSS files..." in output + + +def test_editable_editable(package): + """Test that build_editable() compiles Sass files.""" + # Create a dummy SCSS file + scss_file = package / "package/static/sass/styles.scss" + scss_file.parent.mkdir(parents=True) + scss_file.write_text( + r""" + $color: #abc; + body { + color: $color; + } + """ + ) + + packages_dir = package / "packages" + packages_dir.mkdir(parents=True) + + output = subprocess.check_output( + [ + sys.executable, + "-m", + "pip", + "install", + "--target", + str(packages_dir), + "-v", + "-e", + ".", + ], + cwd=package, + env={**os.environ, "PYTHONPATH": f".:{ROOT}", "PEP517_BACKEND_PATH": str(ROOT)}, + stderr=subprocess.STDOUT, + ) + + # Check that the Sass compilation was logged + assert b"* Compiling Sass/SCSS files..." in output diff --git a/tests/test_scm.py b/tests/test_scm.py new file mode 100644 index 0000000..c99ab6c --- /dev/null +++ b/tests/test_scm.py @@ -0,0 +1,92 @@ +import os +import subprocess # nosec +import sys +from pathlib import Path + +ROOT = Path(__file__).parent.parent + + +def test_build_sdist(package_scm): + """Test that build_sdist() compiles Sass files.""" + # Create a dummy SCSS file + scss_file = package_scm / "package/static/sass/styles.scss" + scss_file.parent.mkdir(parents=True) + scss_file.write_text( + r""" + $color: #abc; + body { + color: $color; + } + """ + ) + + output = subprocess.check_output( + [sys.executable, "-m", "build", "--sdist"], + cwd=package_scm, + env={**os.environ, "PYTHONPATH": f".:{ROOT}"}, + ) + + # Check that the Sass compilation was logged + assert b"* Compiling Sass/CSSS files..." not in output + + +def test_build_wheel(package_scm): + """Test that build_wheel() compiles Sass files.""" + # Create a dummy .po file + scss_file = package_scm / "package/static/sass/styles.scss" + scss_file.parent.mkdir(parents=True) + scss_file.write_text( + r""" + $color: #abc; + body { + color: $color; + } + """ + ) + + output = subprocess.check_output( + [sys.executable, "-m", "build", "--wheel"], + cwd=package_scm, + env={**os.environ, "PYTHONPATH": f".:{ROOT}"}, + ) + + # Check that the Sass compilation was logged + assert b"* Compiling Sass/SCSS files..." in output + + +def test_editable_editable(package_scm): + """Test that build_editable() compiles Sass files.""" + # Create a dummy .po file + scss_file = package_scm / "package/static/sass/styles.scss" + scss_file.parent.mkdir(parents=True) + scss_file.write_text( + r""" + $color: #abc; + body { + color: $color; + } + """ + ) + + packages_dir = package_scm / "packages" + packages_dir.mkdir(parents=True) + + output = subprocess.check_output( + [ + sys.executable, + "-m", + "pip", + "install", + "--target", + str(packages_dir), + "-v", + "-e", + ".", + ], + cwd=package_scm, + env={**os.environ, "PYTHONPATH": f".:{ROOT}", "PEP517_BACKEND_PATH": str(ROOT)}, + stderr=subprocess.STDOUT, + ) + + # Check that the Sass compilation was logged + assert b"* Compiling Sass/SCSS files..." in output diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..4f928ff --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,39 @@ +import pytest +import sass + +import flit_sass.utils + + +def test_compile_sass(capsys, package): + scss_file = package / "package/static/sass/styles.scss" + scss_file.parent.mkdir(parents=True) + scss_file.write_text( + r""" + $color: #abc; + body { + color: $color; + } + """ + ) + + flit_sass.utils.compile_sass() + + assert (package / "package/static/css/styles.css").exists() + captured = capsys.readouterr() + assert captured.out == "\x1b[1m* Compiling Sass/SCSS files...\x1b[0m\n" + assert captured.err == "" + + +def test_compile_sass__invalid(capsys, package): + scss_file = package / "package/static/sass/styles.scss" + scss_file.parent.mkdir(parents=True) + scss_file.write_text( + r""" + body { + color: $var-does-not-exist; + } + """ + ) + + with pytest.raises(sass.CompileError): + flit_sass.utils.compile_sass()