├── tests
├── __init__.py
├── models.py
├── views.py
├── tests
│ ├── models.py
│ ├── __init__.py
│ ├── test_utils.py
│ ├── test_packager.py
│ ├── test_conf.py
│ ├── test_middleware.py
│ ├── test_collector.py
│ ├── test_views.py
│ ├── test_glob.py
│ ├── test_storage.py
│ ├── test_template.py
│ └── test_forms.py
├── templates
│ ├── empty.html
│ └── index.html
├── assets
│ ├── fonts
│ │ ├── pipeline.eot
│ │ ├── pipeline.svg
│ │ ├── pipeline.ttf
│ │ └── pipeline.woff
│ ├── images
│ │ ├── sprite-buttons.png
│ │ ├── arrow.png
│ │ └── embed
│ │ │ └── arrow.png
│ ├── compilers
│ │ ├── stylus
│ │ │ ├── input.styl
│ │ │ └── expected.css
│ │ ├── less
│ │ │ ├── expected.css
│ │ │ └── input.less
│ │ ├── livescript
│ │ │ ├── input.ls
│ │ │ └── expected.js
│ │ ├── coffee
│ │ │ ├── input.coffee
│ │ │ └── expected.js
│ │ ├── scss
│ │ │ ├── expected.css
│ │ │ └── input.scss
│ │ ├── typescript
│ │ │ ├── expected.js
│ │ │ └── input.ts
│ │ └── es6
│ │ │ ├── input.es6
│ │ │ └── expected.js
│ ├── css
│ │ ├── first.css
│ │ ├── second.css
│ │ ├── unicode.css
│ │ ├── sourcemap.css
│ │ ├── urls.css
│ │ └── nested
│ │ │ └── nested.css
│ ├── compressors
│ │ ├── cssmin.css
│ │ ├── yui.css
│ │ ├── csstidy.css
│ │ ├── yuglify.css
│ │ ├── csshtmljsminify.css
│ │ ├── terser.js
│ │ ├── yui.js
│ │ ├── jsmin.js
│ │ ├── yuglify.js
│ │ ├── closure.js
│ │ ├── slimit.js
│ │ ├── uglifyjs.js
│ │ └── csshtmljsminify.js
│ ├── js
│ │ ├── dummy.coffee
│ │ ├── application.js
│ │ ├── second.js
│ │ ├── first.js
│ │ └── sourcemap.js
│ └── templates
│ │ ├── photo
│ │ ├── list.jst
│ │ └── detail.jst
│ │ └── video
│ │ └── detail.jst
├── urls.py
├── utils.py
└── settings.py
├── pipeline
├── templatetags
│ ├── __init__.py
│ └── pipeline.py
├── signals.py
├── jinja2
│ ├── pipeline
│ │ ├── js.jinja
│ │ ├── inline_js.jinja
│ │ └── css.jinja
│ └── __init__.py
├── templates
│ └── pipeline
│ │ ├── js.html
│ │ ├── inline_js.html
│ │ ├── js.jinja
│ │ ├── inline_js.jinja
│ │ ├── css.jinja
│ │ ├── css.html
│ │ └── compile_error.html
├── compressors
│ ├── closure.py
│ ├── cssmin.py
│ ├── jsmin.py
│ ├── terser.py
│ ├── uglifyjs.py
│ ├── csshtmljsminify.py
│ ├── yui.py
│ ├── yuglify.py
│ └── csstidy.py
├── exceptions.py
├── compilers
│ ├── stylus.py
│ ├── sass.py
│ ├── typescript.py
│ ├── es6.py
│ ├── livescript.py
│ ├── less.py
│ ├── coffee.py
│ └── __init__.py
├── __init__.py
├── middleware.py
├── views.py
├── glob.py
├── utils.py
├── collector.py
├── conf.py
├── finders.py
├── storage.py
└── packager.py
├── .flake8
├── .github
├── dependabot.yml
└── workflows
│ ├── release.yml
│ └── test.yml
├── .readthedocs.yaml
├── MANIFEST.in
├── .gitignore
├── .pre-commit-config.yaml
├── docs
├── signals.rst
├── index.rst
├── using.rst
├── installation.rst
├── templates.rst
├── storages.rst
├── Makefile
├── make.bat
├── usage.rst
├── compilers.rst
├── conf.py
└── configuration.rst
├── package.json
├── LICENSE
├── tox.ini
├── CONTRIBUTING.rst
├── CODE_OF_CONDUCT.md
├── pyproject.toml
├── README.rst
├── AUTHORS
└── HISTORY.rst
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/models.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/views.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/tests/models.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/templates/empty.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pipeline/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/assets/fonts/pipeline.eot:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/assets/fonts/pipeline.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/assets/fonts/pipeline.ttf:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/assets/fonts/pipeline.woff:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/assets/images/sprite-buttons.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/assets/compilers/stylus/input.styl:
--------------------------------------------------------------------------------
1 | .a
2 | color: black
--------------------------------------------------------------------------------
/tests/assets/css/first.css:
--------------------------------------------------------------------------------
1 | .concat {
2 | display: none;
3 | }
4 |
--------------------------------------------------------------------------------
/tests/assets/compilers/less/expected.css:
--------------------------------------------------------------------------------
1 | .a {
2 | width: 1px;
3 | }
4 |
--------------------------------------------------------------------------------
/tests/assets/compilers/livescript/input.ls:
--------------------------------------------------------------------------------
1 | times = (x, y) ->
2 | x * y
3 |
--------------------------------------------------------------------------------
/tests/assets/compilers/stylus/expected.css:
--------------------------------------------------------------------------------
1 | .a {
2 | color: #000;
3 | }
4 |
--------------------------------------------------------------------------------
/tests/assets/css/second.css:
--------------------------------------------------------------------------------
1 | .concatenate {
2 | display: block;
3 | }
4 |
--------------------------------------------------------------------------------
/tests/assets/compressors/cssmin.css:
--------------------------------------------------------------------------------
1 | .concat{display:none}.concatenate{display:block}
--------------------------------------------------------------------------------
/tests/assets/compressors/yui.css:
--------------------------------------------------------------------------------
1 | .concat{display:none}.concatenate{display:block}
--------------------------------------------------------------------------------
/tests/assets/compressors/csstidy.css:
--------------------------------------------------------------------------------
1 | .concat{display:none;}.concatenate{display:block;}
--------------------------------------------------------------------------------
/tests/assets/js/dummy.coffee:
--------------------------------------------------------------------------------
1 | square = (x) -> x * x
2 | cube = (x) -> square(x) * x
3 |
--------------------------------------------------------------------------------
/tests/assets/compressors/yuglify.css:
--------------------------------------------------------------------------------
1 | .concat{display:none}.concatenate{display:block}
2 |
--------------------------------------------------------------------------------
/tests/assets/js/application.js:
--------------------------------------------------------------------------------
1 | function test() {
2 | alert('this is a test');
3 | }
4 |
--------------------------------------------------------------------------------
/tests/assets/compilers/less/input.less:
--------------------------------------------------------------------------------
1 | @a: 1;
2 |
3 | .a {
4 | width: (@a + 0px);
5 | }
6 |
--------------------------------------------------------------------------------
/tests/assets/css/unicode.css:
--------------------------------------------------------------------------------
1 | .some_class {
2 | // Some unicode
3 | content: "áéíóú";
4 | }
5 |
--------------------------------------------------------------------------------
/tests/assets/compilers/coffee/input.coffee:
--------------------------------------------------------------------------------
1 | square = (x) -> x * x
2 | cube = (x) -> square(x) * x
3 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 88
3 | exclude =
4 | .tox
5 | node_modules
6 | *env/
7 |
--------------------------------------------------------------------------------
/tests/assets/compressors/csshtmljsminify.css:
--------------------------------------------------------------------------------
1 | @charset "utf-8";.concat{display:none}.concatenate{display:block}
--------------------------------------------------------------------------------
/pipeline/signals.py:
--------------------------------------------------------------------------------
1 | from django.dispatch import Signal
2 |
3 | css_compressed = Signal()
4 | js_compressed = Signal()
5 |
--------------------------------------------------------------------------------
/tests/assets/images/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-pipeline/master/tests/assets/images/arrow.png
--------------------------------------------------------------------------------
/tests/assets/images/embed/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-pipeline/master/tests/assets/images/embed/arrow.png
--------------------------------------------------------------------------------
/tests/assets/js/second.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | window.cat = function() {
3 | console.log("hello world");
4 | }
5 | }());
6 |
--------------------------------------------------------------------------------
/tests/assets/js/first.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | window.concat = function() {
3 | console.log(arguments);
4 | }
5 | }()) // No semicolon
6 |
--------------------------------------------------------------------------------
/tests/assets/templates/photo/list.jst:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 | <%= caption %>
5 |
6 |
--------------------------------------------------------------------------------
/tests/assets/compilers/livescript/expected.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | var times;
3 | times = function(x, y){
4 | return x * y;
5 | };
6 | }).call(this);
7 |
--------------------------------------------------------------------------------
/tests/assets/compressors/terser.js:
--------------------------------------------------------------------------------
1 | (function(){window.concat=function(){console.log(arguments)},window.cat=function(){console.log("hello world")}}).call(this);
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 |
--------------------------------------------------------------------------------
/pipeline/jinja2/pipeline/js.jinja:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/pipeline/templates/pipeline/js.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/assets/templates/video/detail.jst:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= description %>
5 |
6 |
--------------------------------------------------------------------------------
/pipeline/templates/pipeline/inline_js.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pipeline/templates/pipeline/js.jinja:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/pipeline/jinja2/pipeline/inline_js.jinja:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/tests/assets/templates/photo/detail.jst:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 | <%= caption %> by <%= author %>
5 |
6 |
--------------------------------------------------------------------------------
/pipeline/templates/pipeline/inline_js.jinja:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/tests/assets/compilers/scss/expected.css:
--------------------------------------------------------------------------------
1 | .a .b {
2 | display: none;
3 | }
4 |
5 | .c .d {
6 | display: block;
7 | }
8 |
9 | /*# sourceMappingURL=input.css.map */
10 |
--------------------------------------------------------------------------------
/tests/assets/compilers/scss/input.scss:
--------------------------------------------------------------------------------
1 | .a {
2 | .b {
3 | display: none;
4 | }
5 | }
6 | .c {
7 | .d {
8 | display: block;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/assets/compressors/yui.js:
--------------------------------------------------------------------------------
1 | (function(){(function(){window.concat=function(){console.log(arguments)}}());(function(){window.cat=function(){console.log("hello world")}}())}).call(this);
--------------------------------------------------------------------------------
/tests/assets/compressors/jsmin.js:
--------------------------------------------------------------------------------
1 | (function(){(function(){window.concat=function(){console.log(arguments);}}());(function(){window.cat=function(){console.log("hello world");}}());}).call(this);
--------------------------------------------------------------------------------
/tests/assets/compressors/yuglify.js:
--------------------------------------------------------------------------------
1 | (function(){!function(){window.concat=function(){console.log(arguments)}}(),function(){window.cat=function(){console.log("hello world")}}()}).call(this);
2 |
--------------------------------------------------------------------------------
/pipeline/jinja2/pipeline/css.jinja:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/pipeline/templates/pipeline/css.jinja:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tests/assets/compressors/closure.js:
--------------------------------------------------------------------------------
1 | (function(){(function(){window.concat=function(){console.log(arguments)}})();(function(){window.cat=function(){console.log("hello world")}})()}).call(this);
2 |
--------------------------------------------------------------------------------
/tests/assets/compressors/slimit.js:
--------------------------------------------------------------------------------
1 | (function(){(function(){window.concat=function(){console.log(arguments);};}());(function(){window.cat=function(){console.log("hello world");};}());}).call(this);
--------------------------------------------------------------------------------
/tests/assets/compressors/uglifyjs.js:
--------------------------------------------------------------------------------
1 | (function(){(function(){window.concat=function(){console.log(arguments)}})();(function(){window.cat=function(){console.log("hello world")}})()}).call(this);
2 |
--------------------------------------------------------------------------------
/tests/assets/compressors/csshtmljsminify.js:
--------------------------------------------------------------------------------
1 | (function(){(function(){window.concat=function(){console.log(arguments);}}());(function(){window.cat=function(){console.log("hello world");}}());}).call(this);
--------------------------------------------------------------------------------
/tests/assets/compilers/typescript/expected.js:
--------------------------------------------------------------------------------
1 | function getName(u) {
2 | return "".concat(u.firstName, " ").concat(u.lastName);
3 | }
4 | var userName = getName({ firstName: "Django", lastName: "Pipeline" });
5 |
--------------------------------------------------------------------------------
/pipeline/templates/pipeline/css.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/assets/compilers/coffee/expected.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | var cube, square;
3 |
4 | square = function(x) {
5 | return x * x;
6 | };
7 |
8 | cube = function(x) {
9 | return square(x) * x;
10 | };
11 |
12 | }).call(this);
13 |
--------------------------------------------------------------------------------
/tests/templates/index.html:
--------------------------------------------------------------------------------
1 | {% load pipeline %}
2 |
3 |
4 |
5 | Pipeline
6 | {% stylesheet 'screen' %}
7 | {% javascript 'scripts' %}
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # .readthedocs.yaml
2 | # Read the Docs configuration file
3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4 |
5 | version: 2
6 |
7 | build:
8 | os: ubuntu-22.04
9 | tools:
10 | python: "3.10"
11 |
12 | sphinx:
13 | configuration: docs/conf.py
14 |
--------------------------------------------------------------------------------
/pipeline/compressors/closure.py:
--------------------------------------------------------------------------------
1 | from pipeline.compressors import SubProcessCompressor
2 | from pipeline.conf import settings
3 |
4 |
5 | class ClosureCompressor(SubProcessCompressor):
6 | def compress_js(self, js):
7 | command = (settings.CLOSURE_BINARY, settings.CLOSURE_ARGUMENTS)
8 | return self.execute_command(command, js)
9 |
--------------------------------------------------------------------------------
/pipeline/compressors/cssmin.py:
--------------------------------------------------------------------------------
1 | from pipeline.compressors import SubProcessCompressor
2 | from pipeline.conf import settings
3 |
4 |
5 | class CSSMinCompressor(SubProcessCompressor):
6 | def compress_css(self, css):
7 | command = (settings.CSSMIN_BINARY, settings.CSSMIN_ARGUMENTS)
8 | return self.execute_command(command, css)
9 |
--------------------------------------------------------------------------------
/tests/assets/compilers/typescript/input.ts:
--------------------------------------------------------------------------------
1 | type FullName = string;
2 |
3 | interface User {
4 | firstName: string;
5 | lastName: string;
6 | }
7 |
8 |
9 | function getName(u: User): FullName {
10 | return `${u.firstName} ${u.lastName}`;
11 | }
12 |
13 | let userName: FullName = getName({firstName: "Django", lastName: "Pipeline"});
14 |
--------------------------------------------------------------------------------
/tests/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.urls import path
3 | from django.views.generic import TemplateView
4 |
5 | urlpatterns = [
6 | path("", TemplateView.as_view(template_name="index.html"), name="index"),
7 | path("empty/", TemplateView.as_view(template_name="empty.html"), name="empty"),
8 | path("admin/", admin.site.urls),
9 | ]
10 |
--------------------------------------------------------------------------------
/pipeline/compressors/jsmin.py:
--------------------------------------------------------------------------------
1 | from pipeline.compressors import CompressorBase
2 |
3 |
4 | class JSMinCompressor(CompressorBase):
5 | """
6 | JS compressor based on the Python library jsmin
7 | (http://pypi.python.org/pypi/jsmin/).
8 | """
9 |
10 | def compress_js(self, js):
11 | from jsmin import jsmin # noqa: PLC0415
12 |
13 | return jsmin(js)
14 |
--------------------------------------------------------------------------------
/pipeline/compressors/terser.py:
--------------------------------------------------------------------------------
1 | from pipeline.compressors import SubProcessCompressor
2 | from pipeline.conf import settings
3 |
4 |
5 | class TerserCompressor(SubProcessCompressor):
6 | def compress_js(self, js):
7 | command = (settings.TERSER_BINARY, settings.TERSER_ARGUMENTS)
8 | if self.verbose:
9 | command += " --verbose"
10 | return self.execute_command(command, js)
11 |
--------------------------------------------------------------------------------
/pipeline/compressors/uglifyjs.py:
--------------------------------------------------------------------------------
1 | from pipeline.compressors import SubProcessCompressor
2 | from pipeline.conf import settings
3 |
4 |
5 | class UglifyJSCompressor(SubProcessCompressor):
6 | def compress_js(self, js):
7 | command = (settings.UGLIFYJS_BINARY, settings.UGLIFYJS_ARGUMENTS)
8 | if self.verbose:
9 | command += " --verbose"
10 | return self.execute_command(command, js)
11 |
--------------------------------------------------------------------------------
/tests/assets/js/sourcemap.js:
--------------------------------------------------------------------------------
1 | const abc = 123;
2 |
3 |
4 | //# sourceMappingURL=sourcemap1.js.map
5 |
6 | //@ sourceMappingURL=sourcemap2.js.map
7 |
8 | /*# sourceMappingURL=sourcemap3.js.map */
9 |
10 | /*@ sourceMappingURL=sourcemap4.js.map */
11 |
12 | //# sourceURL=sourcemap5.js.map
13 |
14 | //@ sourceURL=sourcemap6.js.map
15 |
16 | /*# sourceURL=sourcemap7.js.map */
17 |
18 | /*@ sourceURL=sourcemap8.js.map */
19 |
--------------------------------------------------------------------------------
/tests/assets/compilers/es6/input.es6:
--------------------------------------------------------------------------------
1 | // Expression bodies
2 | var odds = evens.map(v => v + 1);
3 | var nums = evens.map((v, i) => v + i);
4 |
5 | // Statement bodies
6 | nums.forEach(v => {
7 | if (v % 5 === 0)
8 | fives.push(v);
9 | });
10 |
11 | // Lexical this
12 | var bob = {
13 | _name: "Bob",
14 | _friends: [],
15 | printFriends() {
16 | this._friends.forEach(f =>
17 | console.log(this._name + " knows " + f));
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/pipeline/exceptions.py:
--------------------------------------------------------------------------------
1 | class PipelineException(Exception):
2 | pass
3 |
4 |
5 | class PackageNotFound(PipelineException):
6 | pass
7 |
8 |
9 | class CompilerError(PipelineException):
10 | def __init__(self, msg, command=None, error_output=None):
11 | super().__init__(msg)
12 |
13 | self.command = command
14 | self.error_output = error_output.strip()
15 |
16 |
17 | class CompressorError(PipelineException):
18 | pass
19 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include pipeline/templates *.html *.jinja
2 | recursive-include pipeline/jinja2 *.html *.jinja
3 | include AUTHORS LICENSE README.rst HISTORY.rst CONTRIBUTING.rst
4 | recursive-include tests *
5 | recursive-exclude tests *.pyc *.pyo
6 | recursive-exclude tests/node_modules *
7 | recursive-exclude tests/npm-cache *
8 | recursive-exclude tests/npm *
9 | include docs/Makefile docs/make.bat docs/conf.py
10 | recursive-include docs *.rst
11 | exclude package.json requirements.txt tox.ini
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .AppleDouble
2 | *.pyc
3 | :2e_*
4 | *.tmproj
5 | .*.swp
6 | *.swo
7 | build
8 | dist
9 | MANIFEST
10 | docs/_build/
11 | *.egg-info
12 | .coverage
13 | coverage/
14 | coverage.xml
15 | tests/static/
16 | tests/assets/js/dummy.js
17 | tests/node_modules/
18 | .tox/
19 | .DS_Store
20 | .idea
21 | .venv
22 | .vscode
23 | .project
24 | .pydevproject
25 | .ropeproject
26 | __pycache__
27 | npm-debug.log
28 | tests/npm-cache
29 | django-pipeline-*/
30 | .tags
31 | node_modules/
32 | package-lock.json
33 |
--------------------------------------------------------------------------------
/tests/assets/css/sourcemap.css:
--------------------------------------------------------------------------------
1 | div {
2 | display: inline;
3 | }
4 |
5 | span {
6 | display: block;
7 | }
8 |
9 |
10 | //# sourceMappingURL=sourcemap1.css.map
11 |
12 | //@ sourceMappingURL=sourcemap2.css.map
13 |
14 | /*# sourceMappingURL=sourcemap3.css.map */
15 |
16 | /*@ sourceMappingURL=sourcemap4.css.map */
17 |
18 | //# sourceURL=sourcemap5.css.map
19 |
20 | //@ sourceURL=sourcemap6.css.map
21 |
22 | /*# sourceURL=sourcemap7.css.map */
23 |
24 | /*@ sourceURL=sourcemap8.css.map */
25 |
--------------------------------------------------------------------------------
/tests/tests/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | if sys.platform.startswith("win"):
5 | os.environ.setdefault("NUMBER_OF_PROCESSORS", "1")
6 |
7 |
8 | from .test_collector import * # noqa
9 | from .test_compiler import * # noqa
10 | from .test_compressor import * # noqa
11 | from .test_glob import * # noqa
12 | from .test_middleware import * # noqa
13 | from .test_packager import * # noqa
14 | from .test_storage import * # noqa
15 | from .test_template import * # noqa
16 | from .test_utils import * # noqa
17 | from .test_views import * # noqa
18 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/psf/black
3 | rev: 24.8.0
4 | hooks:
5 | - id: black
6 |
7 | - repo: https://github.com/PyCQA/isort
8 | rev: 5.13.2
9 | hooks:
10 | - id: isort
11 |
12 | - repo: https://github.com/PyCQA/flake8
13 | rev: 7.1.1
14 | hooks:
15 | - id: flake8
16 |
17 | - repo: https://github.com/pre-commit/pre-commit-hooks
18 | rev: v5.0.0
19 | hooks:
20 | - id: check-merge-conflict
21 | - id: check-yaml
22 |
23 | ci:
24 | autoupdate_schedule: quarterly
25 |
--------------------------------------------------------------------------------
/pipeline/compilers/stylus.py:
--------------------------------------------------------------------------------
1 | from os.path import dirname
2 |
3 | from pipeline.compilers import SubProcessCompiler
4 | from pipeline.conf import settings
5 |
6 |
7 | class StylusCompiler(SubProcessCompiler):
8 | output_extension = "css"
9 |
10 | def match_file(self, filename):
11 | return filename.endswith(".styl")
12 |
13 | def compile_file(self, infile, outfile, outdated=False, force=False):
14 | command = (settings.STYLUS_BINARY, settings.STYLUS_ARGUMENTS, infile)
15 | return self.execute_command(command, cwd=dirname(infile))
16 |
--------------------------------------------------------------------------------
/pipeline/compilers/sass.py:
--------------------------------------------------------------------------------
1 | from os.path import dirname
2 |
3 | from pipeline.compilers import SubProcessCompiler
4 | from pipeline.conf import settings
5 |
6 |
7 | class SASSCompiler(SubProcessCompiler):
8 | output_extension = "css"
9 |
10 | def match_file(self, filename):
11 | return filename.endswith((".scss", ".sass"))
12 |
13 | def compile_file(self, infile, outfile, outdated=False, force=False):
14 | command = (settings.SASS_BINARY, settings.SASS_ARGUMENTS, infile, outfile)
15 | return self.execute_command(command, cwd=dirname(infile))
16 |
--------------------------------------------------------------------------------
/pipeline/compressors/csshtmljsminify.py:
--------------------------------------------------------------------------------
1 | from pipeline.compressors import CompressorBase
2 |
3 |
4 | class CssHtmlJsMinifyCompressor(CompressorBase):
5 | """
6 | CSS, HTML and JS compressor based on the Python library css-html-js-minify
7 | (https://pypi.org/project/css-html-js-minify/).
8 | """
9 |
10 | def compress_css(self, css):
11 | from css_html_js_minify import css_minify # noqa: PLC0415
12 |
13 | return css_minify(css)
14 |
15 | def compress_js(self, js):
16 | from css_html_js_minify import js_minify # noqa: PLC0415
17 |
18 | return js_minify(js)
19 |
--------------------------------------------------------------------------------
/pipeline/compressors/yui.py:
--------------------------------------------------------------------------------
1 | from pipeline.compressors import SubProcessCompressor
2 | from pipeline.conf import settings
3 |
4 |
5 | class YUICompressor(SubProcessCompressor):
6 | def compress_common(self, content, compress_type, arguments):
7 | command = (settings.YUI_BINARY, f"--type={compress_type}", arguments)
8 | return self.execute_command(command, content)
9 |
10 | def compress_js(self, js):
11 | return self.compress_common(js, "js", settings.YUI_JS_ARGUMENTS)
12 |
13 | def compress_css(self, css):
14 | return self.compress_common(css, "css", settings.YUI_CSS_ARGUMENTS)
15 |
--------------------------------------------------------------------------------
/tests/assets/compilers/es6/expected.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // Expression bodies
4 | var odds = evens.map(function (v) {
5 | return v + 1;
6 | });
7 | var nums = evens.map(function (v, i) {
8 | return v + i;
9 | });
10 |
11 | // Statement bodies
12 | nums.forEach(function (v) {
13 | if (v % 5 === 0) fives.push(v);
14 | });
15 |
16 | // Lexical this
17 | var bob = {
18 | _name: "Bob",
19 | _friends: [],
20 | printFriends: function printFriends() {
21 | var _this = this;
22 |
23 | this._friends.forEach(function (f) {
24 | return console.log(_this._name + " knows " + f);
25 | });
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/tests/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import mimetypes
2 |
3 | from django.test import TestCase
4 |
5 | from pipeline.utils import guess_type
6 |
7 |
8 | class UtilTest(TestCase):
9 | def test_guess_type(self):
10 | self.assertEqual("text/css", guess_type("stylesheet.css"))
11 | self.assertEqual("text/coffeescript", guess_type("application.coffee"))
12 | self.assertEqual("text/less", guess_type("stylesheet.less"))
13 |
14 | def test_mimetypes_are_str(self):
15 | for ext, mtype in mimetypes.types_map.items():
16 | self.assertIsInstance(ext, str)
17 | self.assertIsInstance(mtype, str)
18 |
--------------------------------------------------------------------------------
/pipeline/compressors/yuglify.py:
--------------------------------------------------------------------------------
1 | from pipeline.compressors import SubProcessCompressor
2 | from pipeline.conf import settings
3 |
4 |
5 | class YuglifyCompressor(SubProcessCompressor):
6 | def compress_common(self, content, compress_type, arguments):
7 | command = (settings.YUGLIFY_BINARY, f"--type={compress_type}", arguments)
8 | return self.execute_command(command, content)
9 |
10 | def compress_js(self, js):
11 | return self.compress_common(js, "js", settings.YUGLIFY_JS_ARGUMENTS)
12 |
13 | def compress_css(self, css):
14 | return self.compress_common(css, "css", settings.YUGLIFY_CSS_ARGUMENTS)
15 |
--------------------------------------------------------------------------------
/tests/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import django
4 | from django.test import override_settings
5 |
6 |
7 | def _(path):
8 | # Make sure the path contains only the correct separator
9 | return path.replace("/", os.sep).replace("\\", os.sep)
10 |
11 |
12 | class pipeline_settings(override_settings):
13 | def __init__(self, **kwargs):
14 | if django.VERSION[:2] >= (1, 10):
15 | # Django 1.10's override_settings inherits from TestContextDecorator
16 | # and its __init__ method calls its superclass' __init__ method too,
17 | # so we must do the same.
18 | super().__init__()
19 | self.options = {"PIPELINE": kwargs}
20 |
--------------------------------------------------------------------------------
/pipeline/compressors/csstidy.py:
--------------------------------------------------------------------------------
1 | from django.core.files import temp as tempfile
2 |
3 | from pipeline.compressors import SubProcessCompressor
4 | from pipeline.conf import settings
5 |
6 |
7 | class CSSTidyCompressor(SubProcessCompressor):
8 | def compress_css(self, css):
9 | output_file = tempfile.NamedTemporaryFile(suffix=".pipeline")
10 |
11 | command = (
12 | settings.CSSTIDY_BINARY,
13 | "-",
14 | settings.CSSTIDY_ARGUMENTS,
15 | output_file.name,
16 | )
17 | self.execute_command(command, css)
18 |
19 | filtered_css = output_file.read()
20 | output_file.close()
21 | return filtered_css
22 |
--------------------------------------------------------------------------------
/pipeline/compilers/typescript.py:
--------------------------------------------------------------------------------
1 | from pipeline.compilers import SubProcessCompiler
2 | from pipeline.conf import settings
3 |
4 |
5 | class TypeScriptCompiler(SubProcessCompiler):
6 | output_extension = "js"
7 |
8 | def match_file(self, path):
9 | return path.endswith(".ts")
10 |
11 | def compile_file(self, infile, outfile, outdated=False, force=False):
12 | if not outdated and not force:
13 | return
14 | command = (
15 | settings.TYPE_SCRIPT_BINARY,
16 | settings.TYPE_SCRIPT_ARGUMENTS,
17 | infile,
18 | "--outFile",
19 | outfile,
20 | )
21 | return self.execute_command(command)
22 |
--------------------------------------------------------------------------------
/pipeline/compilers/es6.py:
--------------------------------------------------------------------------------
1 | from pipeline.compilers import SubProcessCompiler
2 | from pipeline.conf import settings
3 |
4 |
5 | class ES6Compiler(SubProcessCompiler):
6 | output_extension = "js"
7 |
8 | def match_file(self, path):
9 | return path.endswith(".es6")
10 |
11 | def compile_file(self, infile, outfile, outdated=False, force=False):
12 | if not outdated and not force:
13 | return # File doesn't need to be recompiled
14 | command = (
15 | settings.BABEL_BINARY,
16 | settings.BABEL_ARGUMENTS,
17 | infile,
18 | "-o",
19 | outfile,
20 | )
21 | return self.execute_command(command)
22 |
--------------------------------------------------------------------------------
/pipeline/compilers/livescript.py:
--------------------------------------------------------------------------------
1 | from pipeline.compilers import SubProcessCompiler
2 | from pipeline.conf import settings
3 |
4 |
5 | class LiveScriptCompiler(SubProcessCompiler):
6 | output_extension = "js"
7 |
8 | def match_file(self, path):
9 | return path.endswith(".ls")
10 |
11 | def compile_file(self, infile, outfile, outdated=False, force=False):
12 | if not outdated and not force:
13 | return # File doesn't need to be recompiled
14 | command = (
15 | settings.LIVE_SCRIPT_BINARY,
16 | "-cp",
17 | settings.LIVE_SCRIPT_ARGUMENTS,
18 | infile,
19 | )
20 | return self.execute_command(command, stdout_captured=outfile)
21 |
--------------------------------------------------------------------------------
/pipeline/compilers/less.py:
--------------------------------------------------------------------------------
1 | from os.path import dirname
2 |
3 | from pipeline.compilers import SubProcessCompiler
4 | from pipeline.conf import settings
5 |
6 |
7 | class LessCompiler(SubProcessCompiler):
8 | output_extension = "css"
9 |
10 | def match_file(self, filename):
11 | return filename.endswith(".less")
12 |
13 | def compile_file(self, infile, outfile, outdated=False, force=False):
14 | # Pipe to file rather than provide outfile arg due to a bug in lessc
15 | command = (
16 | settings.LESS_BINARY,
17 | settings.LESS_ARGUMENTS,
18 | infile,
19 | )
20 | return self.execute_command(
21 | command, cwd=dirname(infile), stdout_captured=outfile
22 | )
23 |
--------------------------------------------------------------------------------
/pipeline/compilers/coffee.py:
--------------------------------------------------------------------------------
1 | from pipeline.compilers import SubProcessCompiler
2 | from pipeline.conf import settings
3 |
4 |
5 | class CoffeeScriptCompiler(SubProcessCompiler):
6 | output_extension = "js"
7 |
8 | def match_file(self, path):
9 | return path.endswith(".coffee") or path.endswith(".litcoffee")
10 |
11 | def compile_file(self, infile, outfile, outdated=False, force=False):
12 | if not outdated and not force:
13 | return # File doesn't need to be recompiled
14 | command = (
15 | settings.COFFEE_SCRIPT_BINARY,
16 | "-cp",
17 | settings.COFFEE_SCRIPT_ARGUMENTS,
18 | infile,
19 | )
20 | return self.execute_command(command, stdout_captured=outfile)
21 |
--------------------------------------------------------------------------------
/docs/signals.rst:
--------------------------------------------------------------------------------
1 | .. _ref-signals:
2 |
3 | =======
4 | Signals
5 | =======
6 |
7 | List of all signals sent by pipeline.
8 |
9 | css_compressed
10 | --------------
11 |
12 | **pipeline.signals.css_compressed**
13 |
14 | Whenever a css package is compressed, this signal is sent after the compression.
15 |
16 | Arguments sent with this signal :
17 |
18 | :sender:
19 | The ``Packager`` class that compressed the group.
20 |
21 | :package:
22 | The package actually compressed.
23 |
24 |
25 | js_compressed
26 | --------------
27 |
28 | **pipeline.signals.js_compressed**
29 |
30 | Whenever a js package is compressed, this signal is sent after the compression.
31 |
32 | Arguments sent with this signal :
33 |
34 | :sender:
35 | The ``Packager`` class that compressed the group.
36 |
37 | :package:
38 | The package actually compressed.
39 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Introduction
2 | ============
3 |
4 | Pipeline is an asset packaging library for Django, providing
5 | both CSS and JavaScript concatenation and compression, built-in JavaScript
6 | template support, and optional data-URI image and font embedding.
7 |
8 | You can report bugs and discuss features on the `issues page `_.
9 |
10 | You can discuss features or ask questions on the IRC channel on freenode : `#django-pipeline `_
11 |
12 |
13 | Table Of Contents
14 | =================
15 |
16 | .. toctree::
17 | :maxdepth: 2
18 |
19 | installation
20 | configuration
21 | usage
22 | compressors
23 | compilers
24 | templates
25 | storages
26 | signals
27 | using
28 |
29 |
30 | Indices and tables
31 | ==================
32 |
33 | * :ref:`search`
34 |
--------------------------------------------------------------------------------
/pipeline/__init__.py:
--------------------------------------------------------------------------------
1 | PackageNotFoundError = None
2 | DistributionNotFound = None
3 | try:
4 | from importlib.metadata import PackageNotFoundError
5 | from importlib.metadata import version as get_version
6 | except ImportError:
7 | get_version = None
8 | PackageNotFoundError = None
9 | if get_version is None:
10 | try:
11 | from pkg_resources import DistributionNotFound, get_distribution
12 |
13 | def get_version(x):
14 | return get_distribution(x).version
15 |
16 | except ImportError:
17 | get_version = None
18 | DistributionNotFound = None
19 | get_distribution = None
20 |
21 | __version__ = None
22 | if get_version is not None:
23 | try:
24 | __version__ = get_version("django-pipeline")
25 | except PackageNotFoundError:
26 | pass
27 | except DistributionNotFound:
28 | pass
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "django-pipeline-tests",
3 | "private": true,
4 | "version": "1.0.0",
5 | "description": "Pipeline is an asset packaging library for Django.",
6 | "author": "Timothée Peignier ",
7 | "license": "MIT",
8 | "readmeFilename": "../README.rst",
9 | "repository": {
10 | "type": "git",
11 | "url": "git://github.com/jazzband/django-pipeline.git"
12 | },
13 | "dependencies": {
14 | "babel-cli": "latest",
15 | "babel-preset-es2015": "latest",
16 | "coffeescript": "latest",
17 | "less": "latest",
18 | "livescript": "latest",
19 | "sass": "latest",
20 | "stylus": "latest",
21 | "cssmin": "latest",
22 | "google-closure-compiler": "latest",
23 | "terser": "latest",
24 | "uglify-js": "latest",
25 | "yuglify": "1.0.x",
26 | "yuicompressor": "latest",
27 | "typescript": "latest"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/pipeline/middleware.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import MiddlewareNotUsed
2 | from django.utils.deprecation import MiddlewareMixin
3 | from django.utils.encoding import DjangoUnicodeDecodeError
4 | from django.utils.html import strip_spaces_between_tags as minify_html
5 |
6 | from pipeline.conf import settings
7 |
8 |
9 | class MinifyHTMLMiddleware(MiddlewareMixin):
10 | def __init__(self, *args, **kwargs):
11 | super().__init__(*args, **kwargs)
12 | if not settings.PIPELINE_ENABLED:
13 | raise MiddlewareNotUsed
14 |
15 | def process_response(self, request, response):
16 | if (
17 | response.has_header("Content-Type")
18 | and "text/html" in response["Content-Type"]
19 | ):
20 | try:
21 | response.content = minify_html(response.content.decode("utf-8").strip())
22 | response["Content-Length"] = str(len(response.content))
23 | except DjangoUnicodeDecodeError:
24 | pass
25 | return response
26 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 | build:
10 | if: github.repository == 'jazzband/django-pipeline'
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v4
15 | with:
16 | fetch-depth: 0
17 |
18 | - name: Set up Python
19 | uses: actions/setup-python@v5
20 | with:
21 | python-version: '3.9'
22 |
23 | - name: Install dependencies
24 | run: |
25 | python -m pip install -U pip
26 | python -m pip install -U twine build setuptools-scm
27 |
28 | - name: Build package
29 | run: |
30 | python -m setuptools_scm
31 | python -m build
32 | twine check --strict dist/*
33 |
34 | - name: Upload packages to Jazzband
35 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
36 | uses: pypa/gh-action-pypi-publish@release/v1
37 | with:
38 | user: jazzband
39 | password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
40 | repository_url: https://jazzband.co/projects/django-pipeline/upload
41 |
--------------------------------------------------------------------------------
/pipeline/templates/pipeline/compile_error.html:
--------------------------------------------------------------------------------
1 |
3 |
Error compiling {{package_type}} package "{{package_name}}"
4 |
Command:
5 |
{{command}}
6 |
Errors:
7 |
{{errors}}
8 |
9 |
10 |
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (©) 2008 Andreas Pelme
2 | Copyright (©) 2011-2018 Timothée Peignier
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 | THE SOFTWARE.
--------------------------------------------------------------------------------
/tests/assets/css/urls.css:
--------------------------------------------------------------------------------
1 | .embedded-url-svg {
2 | background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E% 3C/svg%3E");
3 | }
4 | @font-face {
5 | font-family: 'Pipeline';
6 | src: url('../fonts/pipeline.eot');
7 | src: url('../fonts/pipeline.eot?#iefix') format('embedded-opentype');
8 | src: local('☺'), url('../fonts/pipeline.woff') format('woff'), url('../fonts/pipeline.ttf') format('truetype'), url('../fonts/pipeline.svg#IyfZbseF') format('svg');
9 | font-weight: normal;
10 | font-style: normal;
11 | }
12 | .relative-url {
13 | background-image: url(../images/sprite-buttons.png);
14 | }
15 | .relative-url-querystring {
16 | background-image: url(../images/sprite-buttons.png?v=1.0#foo=bar);
17 | }
18 | .absolute-url {
19 | background-image: url(/images/sprite-buttons.png);
20 | }
21 | .absolute-full-url {
22 | background-image: url(http://localhost/images/sprite-buttons.png);
23 | }
24 | .no-protocol-url {
25 | background-image: url(//images/sprite-buttons.png);
26 | }
27 | .anchor-tag-url {
28 | background-image: url(#image-gradient);
29 | }
30 | @font-face{src:url(../fonts/pipeline.eot);src:url(../fonts/pipeline.eot?#iefix) format('embedded-opentype'),url(../fonts/pipeline.woff) format('woff'),url(../fonts/pipeline.ttf) format('truetype');}
31 |
--------------------------------------------------------------------------------
/tests/tests/test_packager.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | from pipeline.collector import default_collector
4 | from pipeline.packager import PackageNotFound, Packager
5 | from tests.utils import _
6 |
7 |
8 | class PackagerTest(TestCase):
9 | def setUp(self):
10 | default_collector.collect()
11 |
12 | def test_package_for(self):
13 | packager = Packager()
14 | packager.packages["js"] = packager.create_packages(
15 | {
16 | "application": {
17 | "source_filenames": (_("pipeline/js/application.js"),),
18 | "output_filename": "application.js",
19 | }
20 | }
21 | )
22 | try:
23 | packager.package_for("js", "application")
24 | except PackageNotFound:
25 | self.fail()
26 | try:
27 | packager.package_for("js", "broken")
28 | self.fail()
29 | except PackageNotFound:
30 | pass
31 |
32 | def test_templates(self):
33 | packager = Packager()
34 | packages = packager.create_packages(
35 | {
36 | "templates": {
37 | "source_filenames": (_("pipeline/templates/photo/list.jst"),),
38 | "output_filename": "templates.js",
39 | }
40 | }
41 | )
42 | self.assertEqual(
43 | packages["templates"].templates,
44 | [_("pipeline/templates/photo/list.jst")],
45 | )
46 |
47 | def tearDown(self):
48 | default_collector.clear()
49 |
--------------------------------------------------------------------------------
/tests/tests/test_conf.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from unittest import skipIf, skipUnless
3 |
4 | from django.test import TestCase
5 |
6 | from pipeline.conf import PipelineSettings
7 |
8 |
9 | class TestSettings(TestCase):
10 | def test_3unicode(self):
11 | s = PipelineSettings({"FOO_BINARY": "env actualprogram"})
12 | self.assertEqual(s.FOO_BINARY, ("env", "actualprogram"))
13 |
14 | def test_2unicode(self):
15 | s = PipelineSettings({"FOO_BINARY": "env actualprogram"})
16 | self.assertEqual(s.FOO_BINARY, ("env", "actualprogram"))
17 |
18 | def test_2bytes(self):
19 | s = PipelineSettings({"FOO_BINARY": "env actualprogram"})
20 | self.assertEqual(s.FOO_BINARY, ("env", "actualprogram"))
21 |
22 | def test_expected_splitting(self):
23 | s = PipelineSettings({"FOO_BINARY": "env actualprogram"})
24 | self.assertEqual(s.FOO_BINARY, ("env", "actualprogram"))
25 |
26 | @skipIf(sys.platform.startswith("win"), "requires posix platform")
27 | def test_expected_preservation(self):
28 | s = PipelineSettings({"FOO_BINARY": r"actual\ program"})
29 | self.assertEqual(s.FOO_BINARY, ("actual program",))
30 |
31 | @skipUnless(sys.platform.startswith("win"), "requires windows")
32 | def test_win_path_preservation(self):
33 | s = PipelineSettings({"FOO_BINARY": "C:\\Test\\ActualProgram.exe argument"})
34 | self.assertEqual(s.FOO_BINARY, ("C:\\Test\\ActualProgram.exe", "argument"))
35 |
36 | def test_tuples_are_normal(self):
37 | s = PipelineSettings({"FOO_ARGUMENTS": ("explicit", "with", "args")})
38 | self.assertEqual(s.FOO_ARGUMENTS, ("explicit", "with", "args"))
39 |
--------------------------------------------------------------------------------
/docs/using.rst:
--------------------------------------------------------------------------------
1 | .. _ref-using:
2 |
3 | ====================
4 | Sites using Pipeline
5 | ====================
6 |
7 | The following sites are a partial list of people using Pipeline.
8 |
9 | Are you using pipeline and not being in this list? Drop us a line.
10 |
11 | 20 Minutes
12 | ----------
13 |
14 | For their internal tools: http://www.20minutes.fr
15 |
16 | Borsala
17 | -------
18 |
19 | Borsala is the social investment platform. You can follow stock markets that are traded in Turkey: http://borsala.com
20 |
21 |
22 | Croisé dans le Métro
23 | --------------------
24 |
25 | For their main and mobile website:
26 |
27 | * http://www.croisedanslemetro.com
28 | * http://m.croisedanslemetro.com
29 |
30 | Teachoo
31 | -----------------
32 |
33 | Teachoo uses pipeline for compressing all its static files - https://www.teachoo.com
34 |
35 | The Molly Project
36 | -----------------
37 |
38 | Molly is a framework for the rapid development of information and service
39 | portals targeted at mobile internet devices: https://github.com/mollyproject/mollyproject
40 |
41 | It powers the University of Oxford's mobile portal: http://m.ox.ac.uk/
42 |
43 | Mozilla
44 | -------
45 |
46 | * `mozilla.org `_ (https://github.com/mozilla/bedrock)
47 | * `Mozilla Developer Network `_ (https://github.com/mozilla/kuma)
48 |
49 | Novapost
50 | --------
51 |
52 | For PeopleDoc suite products: http://www.people-doc.com/
53 |
54 | Sophicware
55 | ----------
56 |
57 | Sophicware offers web hosting and DevOps as a service: http://sophicware.com
58 |
59 | Ulule
60 | -----
61 |
62 | For their main and forum website:
63 |
64 | * http://www.ulule.com
65 | * http://vox.ulule.com
66 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist =
3 | pypy3-dj{41,42}
4 | py{39,310,311}-dj41
5 | py{39,310,311,312}-dj42
6 | py{310,311,312}-dj50
7 | py{310,311,312}-dj51
8 | py{310,311,312,313}-dj52
9 | py{310,311,312,313}-djmain
10 | docs
11 |
12 | [gh-actions]
13 | python =
14 | 3.9: py39, docs
15 | 3.10: py310
16 | 3.11: py311
17 | 3.12: py312
18 | 3.13: py313
19 | pypy3: pypy3
20 |
21 | [gh-actions:env]
22 | DJANGO =
23 | 4.1: dj41
24 | 4.2: dj42
25 | 5.0: dj50
26 | 5.1: dj51
27 | 5.2: dj52
28 | main: djmain
29 |
30 | [testenv]
31 | basepython =
32 | pypy3: pypy3
33 | py39: python3.9
34 | py310: python3.10
35 | py311: python3.11
36 | py312: python3.12
37 | py313: python3.13
38 | deps =
39 | pypy3: mock
40 | dj32: Django>=3.2,<3.3
41 | dj41: Django>=4.1,<4.2
42 | dj42: Django>=4.2,<4.3
43 | dj50: Django>=5.0,<5.1
44 | dj51: Django>=5.1,<5.2
45 | dj52: Django>=5.2,<5.3
46 | djmain: https://github.com/django/django/archive/main.tar.gz
47 | jinja2
48 | coverage
49 | jsmin==3.0.0
50 | ply==3.4
51 | css-html-js-minify==2.5.5
52 | setenv =
53 | DJANGO_SETTINGS_MODULE = tests.settings
54 | PYTHONPATH = {toxinidir}
55 | commands =
56 | npm install
57 | {envbindir}/coverage run --source pipeline {envbindir}/django-admin test {posargs:tests}
58 | {envbindir}/coverage report
59 | {envbindir}/coverage xml
60 | whitelist_externals = npm
61 | ignore_outcome =
62 | djmain: True
63 | ignore_errors =
64 | djmain: True
65 | allowlist_externals=npm
66 |
67 | [testenv:docs]
68 | basepython = python3.9
69 | changedir = docs
70 | deps = sphinx
71 | commands =
72 | {envbindir}/sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
73 |
--------------------------------------------------------------------------------
/pipeline/views.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings as django_settings
2 | from django.core.exceptions import ImproperlyConfigured
3 | from django.views.static import serve
4 |
5 | from .collector import default_collector
6 | from .conf import settings
7 |
8 |
9 | def serve_static(request, path, insecure=False, **kwargs):
10 | """Collect and serve static files.
11 |
12 | This view serves up static files, much like Django's
13 | :py:func:`~django.views.static.serve` view, with the addition that it
14 | collects static files first (if enabled). This allows images, fonts, and
15 | other assets to be served up without first loading a page using the
16 | ``{% javascript %}`` or ``{% stylesheet %}`` template tags.
17 |
18 | You can use this view by adding the following to any :file:`urls.py`::
19 |
20 | urlpatterns += static('static/', view='pipeline.views.serve_static')
21 | """
22 | # Follow the same logic Django uses for determining access to the
23 | # static-serving view.
24 | if not django_settings.DEBUG and not insecure:
25 | raise ImproperlyConfigured(
26 | "The staticfiles view can only be used in "
27 | "debug mode or if the --insecure "
28 | "option of 'runserver' is used"
29 | )
30 |
31 | if not settings.PIPELINE_ENABLED and settings.PIPELINE_COLLECTOR_ENABLED:
32 | # Collect only the requested file, in order to serve the result as
33 | # fast as possible. This won't interfere with the template tags in any
34 | # way, as those will still cause Django to collect all media.
35 | default_collector.collect(request, files=[path])
36 |
37 | return serve(request, path, document_root=django_settings.STATIC_ROOT, **kwargs)
38 |
--------------------------------------------------------------------------------
/tests/tests/test_middleware.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import patch
2 |
3 | from django.core.exceptions import MiddlewareNotUsed
4 | from django.http import HttpRequest, HttpResponse
5 | from django.test import TestCase
6 |
7 | from pipeline.middleware import MinifyHTMLMiddleware
8 |
9 |
10 | def dummy_get_response(request):
11 | return None
12 |
13 |
14 | class MiddlewareTest(TestCase):
15 | whitespace = b" "
16 |
17 | def setUp(self):
18 | self.req = HttpRequest()
19 | self.req.META = {
20 | "SERVER_NAME": "testserver",
21 | "SERVER_PORT": 80,
22 | }
23 | self.req.path = self.req.path_info = "/"
24 | self.resp = HttpResponse()
25 | self.resp.status_code = 200
26 | self.resp.content = self.whitespace
27 |
28 | def test_middleware_html(self):
29 | self.resp["Content-Type"] = "text/html; charset=UTF-8"
30 |
31 | response = MinifyHTMLMiddleware(dummy_get_response).process_response(
32 | self.req, self.resp
33 | )
34 | self.assertIn("text/html", response["Content-Type"])
35 | self.assertNotIn(self.whitespace, response.content)
36 |
37 | def test_middleware_text(self):
38 | self.resp["Content-Type"] = "text/plain; charset=UTF-8"
39 |
40 | response = MinifyHTMLMiddleware(dummy_get_response).process_response(
41 | self.req, self.resp
42 | )
43 | self.assertIn("text/plain", response["Content-Type"])
44 | self.assertIn(self.whitespace, response.content)
45 |
46 | @patch("pipeline.middleware.settings.PIPELINE_ENABLED", False)
47 | def test_middleware_not_used(self):
48 | self.assertRaises(MiddlewareNotUsed, MinifyHTMLMiddleware, dummy_get_response)
49 |
--------------------------------------------------------------------------------
/CONTRIBUTING.rst:
--------------------------------------------------------------------------------
1 | .. image:: https://jazzband.co/static/img/jazzband.svg
2 | :target: https://jazzband.co/
3 | :alt: Jazzband
4 |
5 | This is a `Jazzband `_ project. By contributing you agree to abide by the `Contributor Code of Conduct `_ and follow the `guidelines `_.
6 |
7 | Contribute
8 | ==========
9 |
10 | #. Check for open issues or open a fresh issue to start a discussion around a
11 | feature idea or a bug. There is a **contribute!** tag for issues that should be
12 | ideal for people who are not very familiar with the codebase yet.
13 | #. Fork the repository on Github to start making your changes on a topic branch.
14 | #. Write a test which shows that the bug was fixed or that the feature works as expected.
15 | #. Send a pull request and bug the maintainer until it gets merged and published.
16 | Make sure to add yourself to *AUTHORS*.
17 |
18 | Otherwise, if you simply wants to suggest a feature or report a bug, create an issue :
19 | https://github.com/jazzband/django-pipeline/issues
20 |
21 |
22 | Running tests
23 | =============
24 |
25 | We use tox to run the test suite on different versions locally (and GitHub Actions
26 | to automate the check for PRs).
27 |
28 | To tun the test suite locally, please make sure your python environment has
29 | tox and django installed::
30 |
31 | python3.7 -m pip install tox
32 |
33 | Since we use a number of node.js tools, one should first install the node
34 | dependencies. We recommend using [nvm](https://github.com/nvm-sh/nvm#installation-and-update) , tl;dr::
35 |
36 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.2/install.sh | bash
37 | nvm install node
38 | nvm use node
39 |
40 | And then simply execute tox to run the whole test matrix::
41 |
42 | tox
43 |
--------------------------------------------------------------------------------
/pipeline/glob.py:
--------------------------------------------------------------------------------
1 | import fnmatch
2 | import os
3 | import re
4 |
5 | from django.contrib.staticfiles.storage import staticfiles_storage
6 |
7 | __all__ = ["glob", "iglob"]
8 |
9 |
10 | def glob(pathname):
11 | """Return a list of paths matching a pathname pattern.
12 |
13 | The pattern may contain simple shell-style wildcards a la fnmatch.
14 |
15 | """
16 | return sorted(list(iglob(pathname)))
17 |
18 |
19 | def iglob(pathname):
20 | """Return an iterator which yields the paths matching a pathname pattern.
21 |
22 | The pattern may contain simple shell-style wildcards a la fnmatch.
23 |
24 | """
25 | if not has_magic(pathname):
26 | yield pathname
27 | return
28 | dirname, basename = os.path.split(pathname)
29 | if not dirname:
30 | for name in glob1(dirname, basename):
31 | yield name
32 | return
33 | if has_magic(dirname):
34 | dirs = iglob(dirname)
35 | else:
36 | dirs = [dirname]
37 | if has_magic(basename):
38 | glob_in_dir = glob1
39 | else:
40 | glob_in_dir = glob0
41 | for dirname in dirs:
42 | for name in glob_in_dir(dirname, basename):
43 | yield os.path.join(dirname, name)
44 |
45 |
46 | # These 2 helper functions non-recursively glob inside a literal directory.
47 | # They return a list of basenames. `glob1` accepts a pattern while `glob0`
48 | # takes a literal basename (so it only has to check for its existence).
49 |
50 |
51 | def glob1(dirname, pattern):
52 | try:
53 | directories, files = staticfiles_storage.listdir(dirname)
54 | names = directories + files
55 | except Exception:
56 | # We are not sure that dirname is a real directory
57 | # and storage implementations are really exotic.
58 | return []
59 | if pattern[0] != ".":
60 | names = [x for x in names if x[0] != "."]
61 | return fnmatch.filter(names, pattern)
62 |
63 |
64 | def glob0(dirname, basename):
65 | if staticfiles_storage.exists(os.path.join(dirname, basename)):
66 | return [basename]
67 | return []
68 |
69 |
70 | magic_check = re.compile("[*?[]")
71 |
72 |
73 | def has_magic(s):
74 | return magic_check.search(s) is not None
75 |
--------------------------------------------------------------------------------
/pipeline/utils.py:
--------------------------------------------------------------------------------
1 | try:
2 | import fcntl
3 | except ImportError:
4 | # windows
5 | fcntl = None
6 |
7 | import importlib
8 | import mimetypes
9 | import os
10 | import posixpath
11 | import sys
12 | from urllib.parse import quote
13 |
14 | from django.utils.encoding import smart_str
15 |
16 | from pipeline.conf import settings
17 |
18 |
19 | def to_class(class_str):
20 | if not class_str:
21 | return None
22 |
23 | module_bits = class_str.split(".")
24 | module_path, class_name = ".".join(module_bits[:-1]), module_bits[-1]
25 | module = importlib.import_module(module_path)
26 | return getattr(module, class_name, None)
27 |
28 |
29 | def filepath_to_uri(path):
30 | if path is None:
31 | return path
32 | return quote(smart_str(path).replace("\\", "/"), safe="/~!*()'#?")
33 |
34 |
35 | def guess_type(path, default=None):
36 | for type, ext in settings.MIMETYPES:
37 | mimetypes.add_type(type, ext)
38 | mimetype, _ = mimetypes.guess_type(path)
39 | if not mimetype:
40 | return default
41 | return smart_str(mimetype)
42 |
43 |
44 | def relpath(path, start=posixpath.curdir):
45 | """Return a relative version of a path"""
46 | if not path:
47 | raise ValueError("no path specified")
48 |
49 | start_list = posixpath.abspath(start).split(posixpath.sep)
50 | path_list = posixpath.abspath(path).split(posixpath.sep)
51 |
52 | # Work out how much of the filepath is shared by start and path.
53 | i = len(posixpath.commonprefix([start_list, path_list]))
54 |
55 | rel_list = [posixpath.pardir] * (len(start_list) - i) + path_list[i:]
56 | if not rel_list:
57 | return posixpath.curdir
58 | return posixpath.join(*rel_list)
59 |
60 |
61 | def set_std_streams_blocking():
62 | """
63 | Set stdout and stderr to be blocking.
64 |
65 | This is called after Popen.communicate() to revert stdout and stderr back
66 | to be blocking (the default) in the event that the process to which they
67 | were passed manipulated one or both file descriptors to be non-blocking.
68 | """
69 | if not fcntl:
70 | return
71 | for f in (sys.__stdout__, sys.__stderr__):
72 | fileno = f.fileno()
73 | flags = fcntl.fcntl(fileno, fcntl.F_GETFL)
74 | fcntl.fcntl(fileno, fcntl.F_SETFL, flags & ~os.O_NONBLOCK)
75 |
--------------------------------------------------------------------------------
/tests/assets/css/nested/nested.css:
--------------------------------------------------------------------------------
1 | .data-url {
2 | background-image: url(data:image/svg+xml;charset=US-ASCII,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22iso-8859-1%22%3F%3E%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%3Csvg%20version%3D%221.1%22%20id%3D%22Layer_1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%20%20width%3D%2212px%22%20height%3D%2214px%22%20viewBox%3D%220%200%2012%2014%22%20style%3D%22enable-background%3Anew%200%200%2012%2014%3B%22%20xml%3Aspace%3D%22preserve%22%3E%3Cpath%20d%3D%22M11%2C6V5c0-2.762-2.239-5-5-5S1%2C2.238%2C1%2C5v1H0v8h12V6H11z%20M6.5%2C9.847V12h-1V9.847C5.207%2C9.673%2C5%2C9.366%2C5%2C9%20c0-0.553%2C0.448-1%2C1-1s1%2C0.447%2C1%2C1C7%2C9.366%2C6.793%2C9.673%2C6.5%2C9.847z%20M9%2C6H3V5c0-1.657%2C1.343-3%2C3-3s3%2C1.343%2C3%2C3V6z%22%2F%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3C%2Fsvg%3E);
3 | }
4 | .data-url-quoted {
5 | background-image: url('data:image/svg+xml;charset=US-ASCII,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22iso-8859-1%22%3F%3E%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%3Csvg%20version%3D%221.1%22%20id%3D%22Layer_1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%20%20width%3D%2212px%22%20height%3D%2214px%22%20viewBox%3D%220%200%2012%2014%22%20style%3D%22enable-background%3Anew%200%200%2012%2014%3B%22%20xml%3Aspace%3D%22preserve%22%3E%3Cpath%20d%3D%22M11%2C6V5c0-2.762-2.239-5-5-5S1%2C2.238%2C1%2C5v1H0v8h12V6H11z%20M6.5%2C9.847V12h-1V9.847C5.207%2C9.673%2C5%2C9.366%2C5%2C9%20c0-0.553%2C0.448-1%2C1-1s1%2C0.447%2C1%2C1C7%2C9.366%2C6.793%2C9.673%2C6.5%2C9.847z%20M9%2C6H3V5c0-1.657%2C1.343-3%2C3-3s3%2C1.343%2C3%2C3V6z%22%2F%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3C%2Fsvg%3E');
6 | }
7 |
--------------------------------------------------------------------------------
/docs/installation.rst:
--------------------------------------------------------------------------------
1 | .. _ref-installation:
2 |
3 | ============
4 | Installation
5 | ============
6 |
7 | 1. Either check out Pipeline from GitHub_ or to pull a release off PyPI_ ::
8 |
9 | pip install django-pipeline
10 |
11 |
12 | 2. Add 'pipeline' to your ``INSTALLED_APPS`` ::
13 |
14 | INSTALLED_APPS = (
15 | 'pipeline',
16 | )
17 |
18 | 3. Use a pipeline storage for ``STATICFILES_STORAGE`` ::
19 |
20 | STATICFILES_STORAGE = 'pipeline.storage.PipelineManifestStorage'
21 |
22 | 4. Add the ``PipelineFinder`` to ``STATICFILES_FINDERS`` ::
23 |
24 | STATICFILES_FINDERS = (
25 | 'django.contrib.staticfiles.finders.FileSystemFinder',
26 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
27 | 'pipeline.finders.PipelineFinder',
28 | )
29 |
30 |
31 | .. note::
32 | You need to use ``Django>=1.11`` to be able to use this version of pipeline.
33 |
34 | .. _GitHub: http://github.com/jazzband/django-pipeline
35 | .. _PyPI: http://pypi.python.org/pypi/django-pipeline
36 |
37 | Upgrading from 1.3
38 | ==================
39 |
40 | To upgrade from pipeline 1.3, you will need to follow these steps:
41 |
42 | 1. Update templates to use the new syntax
43 |
44 | .. code-block:: python
45 |
46 | {# pipeline<1.4 #}
47 | {% load compressed %}
48 | {% compressed_js 'group' %}
49 | {% compressed_css 'group' %}
50 |
51 | .. code-block:: python
52 |
53 | {# pipeline>=1.4 #}
54 | {% load pipeline %}
55 | {% javascript 'group' %}
56 | {% stylesheet 'group' %}
57 |
58 | 2. Add the ``PipelineFinder`` to ``STATICFILES_FINDERS`` ::
59 |
60 | STATICFILES_FINDERS = (
61 | 'django.contrib.staticfiles.finders.FileSystemFinder',
62 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
63 | 'pipeline.finders.PipelineFinder',
64 | )
65 |
66 |
67 | Upgrading from 1.5
68 | ==================
69 |
70 | To upgrade from pipeline 1.5, you will need update all your ``PIPELINE_*``
71 | settings and move them under the new ``PIPELINE`` setting.
72 | See :ref:`ref-configuration`.
73 |
74 | Recommendations
75 | ===============
76 |
77 | Pipeline's default CSS and JS compressor is Yuglify.
78 | Yuglify wraps UglifyJS and cssmin, applying the default YUI configurations to them.
79 | It can be downloaded from: https://github.com/yui/yuglify/.
80 |
81 | If you do not install yuglify, make sure to disable the compressor in your settings.
82 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | As contributors and maintainers of the Jazzband projects, and in the interest of
4 | fostering an open and welcoming community, we pledge to respect all people who
5 | contribute through reporting issues, posting feature requests, updating documentation,
6 | submitting pull requests or patches, and other activities.
7 |
8 | We are committed to making participation in the Jazzband a harassment-free experience
9 | for everyone, regardless of the level of experience, gender, gender identity and
10 | expression, sexual orientation, disability, personal appearance, body size, race,
11 | ethnicity, age, religion, or nationality.
12 |
13 | Examples of unacceptable behavior by participants include:
14 |
15 | - The use of sexualized language or imagery
16 | - Personal attacks
17 | - Trolling or insulting/derogatory comments
18 | - Public or private harassment
19 | - Publishing other's private information, such as physical or electronic addresses,
20 | without explicit permission
21 | - Other unethical or unprofessional conduct
22 |
23 | The Jazzband roadies have the right and responsibility to remove, edit, or reject
24 | comments, commits, code, wiki edits, issues, and other contributions that are not
25 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor
26 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
27 |
28 | By adopting this Code of Conduct, the roadies commit themselves to fairly and
29 | consistently applying these principles to every aspect of managing the jazzband
30 | projects. Roadies who do not follow or enforce the Code of Conduct may be permanently
31 | removed from the Jazzband roadies.
32 |
33 | This code of conduct applies both within project spaces and in public spaces when an
34 | individual is representing the project or its community.
35 |
36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
37 | contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and
38 | investigated and will result in a response that is deemed necessary and appropriate to
39 | the circumstances. Roadies are obligated to maintain confidentiality with regard to the
40 | reporter of an incident.
41 |
42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version
43 | 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version]
44 |
45 | [homepage]: https://contributor-covenant.org
46 | [version]: https://contributor-covenant.org/version/1/3/0/
47 |
--------------------------------------------------------------------------------
/tests/tests/test_collector.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django.contrib.staticfiles import finders
4 | from django.core.files.storage import FileSystemStorage
5 | from django.test import TestCase
6 |
7 | from pipeline.collector import default_collector
8 | from pipeline.finders import PipelineFinder
9 |
10 |
11 | def local_path(path):
12 | return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", path))
13 |
14 |
15 | class CollectorTest(TestCase):
16 | def tearDown(self):
17 | super().tearDown()
18 |
19 | default_collector.clear()
20 |
21 | def test_collect(self):
22 | self.assertEqual(
23 | set(default_collector.collect()), set(self._get_collectable_files())
24 | )
25 |
26 | def test_collect_with_files(self):
27 | self.assertEqual(
28 | set(
29 | default_collector.collect(
30 | files=[
31 | "pipeline/js/first.js",
32 | "pipeline/js/second.js",
33 | ]
34 | )
35 | ),
36 | {
37 | "pipeline/js/first.js",
38 | "pipeline/js/second.js",
39 | },
40 | )
41 |
42 | def test_delete_file_with_modified(self):
43 | list(default_collector.collect())
44 |
45 | storage = FileSystemStorage(local_path("assets"))
46 | new_mtime = os.path.getmtime(storage.path("js/first.js")) - 1000
47 | os.utime(
48 | default_collector.storage.path("pipeline/js/first.js"),
49 | (new_mtime, new_mtime),
50 | )
51 |
52 | self.assertTrue(
53 | default_collector.delete_file(
54 | "js/first.js", "pipeline/js/first.js", storage
55 | )
56 | )
57 |
58 | def test_delete_file_with_unmodified(self):
59 | list(default_collector.collect(files=["pipeline/js/first.js"]))
60 |
61 | self.assertFalse(
62 | default_collector.delete_file(
63 | "js/first.js",
64 | "pipeline/js/first.js",
65 | FileSystemStorage(local_path("assets")),
66 | )
67 | )
68 |
69 | def _get_collectable_files(self):
70 | for finder in finders.get_finders():
71 | if not isinstance(finder, PipelineFinder):
72 | for path, storage in finder.list(["CVS", ".*", "*~"]):
73 | if getattr(storage, "prefix", None):
74 | yield os.path.join(storage.prefix, path)
75 | else:
76 | yield path
77 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=64", "setuptools_scm[toml]>=8"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "django-pipeline"
7 | requires-python = ">=3.9"
8 | version = "4.1.0"
9 | description = "Pipeline is an asset packaging library for Django."
10 | readme = "README.rst"
11 | authors = [{ "name" = "Timothée Peignier", "email" = "timothee.peignier@tryphon.org" }]
12 | license = { text = "MIT" }
13 | classifiers = [
14 | "Development Status :: 5 - Production/Stable",
15 | "Environment :: Web Environment",
16 | "Framework :: Django",
17 | "Framework :: Django :: 4.0",
18 | "Framework :: Django :: 4.1",
19 | "Framework :: Django :: 4.2",
20 | "Framework :: Django :: 5.0",
21 | "Framework :: Django :: 5.1",
22 | "Framework :: Django :: 5.2",
23 | "Intended Audience :: Developers",
24 | "License :: OSI Approved :: MIT License",
25 | "Operating System :: OS Independent",
26 | "Programming Language :: Python",
27 | "Programming Language :: Python :: 3",
28 | "Programming Language :: Python :: 3 :: Only",
29 | "Programming Language :: Python :: 3.9",
30 | "Programming Language :: Python :: 3.10",
31 | "Programming Language :: Python :: 3.11",
32 | "Programming Language :: Python :: 3.12",
33 | "Programming Language :: Python :: 3.13",
34 | "Programming Language :: Python :: Implementation :: PyPy",
35 | "Topic :: Utilities",
36 | "Topic :: Software Development :: Libraries :: Python Modules",
37 | "Topic :: Internet :: WWW/HTTP",
38 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
39 | ]
40 | keywords = [
41 | "django",
42 | "pipeline",
43 | "asset",
44 | "compiling",
45 | "concatenation",
46 | "compression",
47 | "packaging",
48 | ]
49 | dependencies = [
50 | # indirect dependencies
51 | "setuptools",
52 | "wheel",
53 | ]
54 |
55 | [project.optional-dependencies]
56 | testing = [
57 | "coveralls",
58 | "tox",
59 | "wheel",
60 | "django",
61 | ]
62 |
63 | [project.urls]
64 | homepage = "https://github.com/jazzband/django-pipeline/"
65 | documentation = "https://django-pipeline.readthedocs.io/"
66 | repository = "https://github.com/jazzband/django-pipeline"
67 | changelog = "https://github.com/jazzband/django-pipeline/blob/master/HISTORY.rst"
68 |
69 | [tool.setuptools]
70 | include-package-data = true
71 |
72 | [tool.setuptools.packages.find]
73 | exclude = ["tests", "tests.tests"]
74 |
75 | [tool.setuptools_scm]
76 | local_scheme = "dirty-tag"
77 |
78 | [tool.black]
79 | line-length = 88
80 | target-version = ["py39"]
81 |
82 | [tool.isort]
83 | profile = "black"
84 |
--------------------------------------------------------------------------------
/tests/tests/test_views.py:
--------------------------------------------------------------------------------
1 | from django.contrib.staticfiles.storage import staticfiles_storage
2 | from django.core.exceptions import ImproperlyConfigured
3 | from django.http import Http404
4 | from django.test import RequestFactory, TestCase
5 | from django.test.utils import override_settings
6 |
7 | from pipeline.collector import default_collector
8 | from pipeline.views import serve_static
9 | from tests.utils import pipeline_settings
10 |
11 |
12 | @override_settings(DEBUG=True)
13 | @pipeline_settings(PIPELINE_COLLECTOR_ENABLED=True, PIPELINE_ENABLED=False)
14 | class ServeStaticViewsTest(TestCase):
15 | def setUp(self):
16 | super().setUp()
17 |
18 | self.filename = "pipeline/js/first.js"
19 | self.storage = staticfiles_storage
20 | self.request = RequestFactory().get(f"/static/{self.filename}")
21 |
22 | default_collector.clear()
23 |
24 | def tearDown(self):
25 | super().tearDown()
26 |
27 | default_collector.clear()
28 | staticfiles_storage._setup()
29 |
30 | def test_found(self):
31 | self._test_found()
32 |
33 | def test_not_found(self):
34 | self._test_not_found("missing-file")
35 |
36 | @override_settings(DEBUG=False)
37 | def test_debug_false(self):
38 | with self.assertRaises(ImproperlyConfigured):
39 | serve_static(self.request, self.filename)
40 |
41 | self.assertFalse(self.storage.exists(self.filename))
42 |
43 | @override_settings(DEBUG=False)
44 | def test_debug_false_and_insecure(self):
45 | self._test_found(insecure=True)
46 |
47 | @pipeline_settings(PIPELINE_ENABLED=True)
48 | def test_pipeline_enabled_and_found(self):
49 | self._write_content()
50 | self._test_found()
51 |
52 | @pipeline_settings(PIPELINE_ENABLED=True)
53 | def test_pipeline_enabled_and_not_found(self):
54 | self._test_not_found(self.filename)
55 |
56 | @pipeline_settings(PIPELINE_COLLECTOR_ENABLED=False)
57 | def test_collector_disabled_and_found(self):
58 | self._write_content()
59 | self._test_found()
60 |
61 | @pipeline_settings(PIPELINE_COLLECTOR_ENABLED=False)
62 | def test_collector_disabled_and_not_found(self):
63 | self._test_not_found(self.filename)
64 |
65 | def _write_content(self, content="abc123"):
66 | """Write sample content to the test static file."""
67 | with self.storage.open(self.filename, "w") as f:
68 | f.write(content)
69 |
70 | def _test_found(self, **kwargs):
71 | """Test that a file can be found and contains the correct content."""
72 | response = serve_static(self.request, self.filename, **kwargs)
73 | self.assertEqual(response.status_code, 200)
74 | self.assertTrue(self.storage.exists(self.filename))
75 |
76 | if hasattr(response, "streaming_content"):
77 | content = b"".join(response.streaming_content)
78 | else:
79 | content = response.content
80 |
81 | with self.storage.open(self.filename) as f:
82 | self.assertEqual(f.read(), content)
83 |
84 | def _test_not_found(self, filename):
85 | """Test that a file could not be found."""
86 | self.assertFalse(self.storage.exists(filename))
87 |
88 | with self.assertRaises(Http404):
89 | serve_static(self.request, filename)
90 |
91 | self.assertFalse(self.storage.exists(filename))
92 |
--------------------------------------------------------------------------------
/pipeline/jinja2/__init__.py:
--------------------------------------------------------------------------------
1 | from django.contrib.staticfiles.storage import staticfiles_storage
2 | from jinja2 import TemplateSyntaxError, nodes
3 | from jinja2.ext import Extension
4 |
5 | from ..packager import PackageNotFound
6 | from ..templatetags.pipeline import PipelineMixin
7 | from ..utils import guess_type
8 |
9 |
10 | class PipelineExtension(PipelineMixin, Extension):
11 | tags = {"stylesheet", "javascript"}
12 |
13 | def parse(self, parser):
14 | tag = next(parser.stream)
15 |
16 | package_name = parser.parse_expression()
17 | if not package_name:
18 | raise TemplateSyntaxError("Bad package name", tag.lineno)
19 |
20 | args = [package_name]
21 | if tag.value == "stylesheet":
22 | return nodes.CallBlock(
23 | self.call_method("package_css", args), [], [], []
24 | ).set_lineno(tag.lineno)
25 |
26 | if tag.value == "javascript":
27 | return nodes.CallBlock(
28 | self.call_method("package_js", args), [], [], []
29 | ).set_lineno(tag.lineno)
30 |
31 | return []
32 |
33 | def package_css(self, package_name, *args, **kwargs):
34 | try:
35 | package = self.package_for(package_name, "css")
36 | except PackageNotFound:
37 | # fail silently, do not return anything if an invalid group is specified
38 | return ""
39 | return self.render_compressed(package, package_name, "css")
40 |
41 | def render_css(self, package, path):
42 | template_name = package.template_name or "pipeline/css.jinja"
43 | context = package.extra_context
44 | context.update(
45 | {"type": guess_type(path, "text/css"), "url": staticfiles_storage.url(path)}
46 | )
47 | template = self.environment.get_template(template_name)
48 | return template.render(context)
49 |
50 | def render_individual_css(self, package, paths, **kwargs):
51 | tags = [self.render_css(package, path) for path in paths]
52 | return "\n".join(tags)
53 |
54 | def package_js(self, package_name, *args, **kwargs):
55 | try:
56 | package = self.package_for(package_name, "js")
57 | except PackageNotFound:
58 | # fail silently, do not return anything if an invalid group is specified
59 | return ""
60 | return self.render_compressed(package, package_name, "js")
61 |
62 | def render_js(self, package, path):
63 | template_name = package.template_name or "pipeline/js.jinja"
64 | context = package.extra_context
65 | context.update(
66 | {
67 | "type": guess_type(path, "text/javascript"),
68 | "url": staticfiles_storage.url(path),
69 | }
70 | )
71 | template = self.environment.get_template(template_name)
72 | return template.render(context)
73 |
74 | def render_inline(self, package, js):
75 | context = package.extra_context
76 | context.update({"source": js})
77 | template = self.environment.get_template("pipeline/inline_js.jinja")
78 | return template.render(context)
79 |
80 | def render_individual_js(self, package, paths, templates=None):
81 | tags = [self.render_js(package, js) for js in paths]
82 | if templates:
83 | tags.append(self.render_inline(package, templates))
84 | return "\n".join(tags)
85 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | name: build (Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }})
8 | runs-on: ubuntu-latest
9 | strategy:
10 | fail-fast: false
11 | matrix:
12 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.10']
13 | django-version: ['4.1', '4.2', '5.0', '5.1', '5.2', 'main']
14 | exclude:
15 | - python-version: '3.9'
16 | django-version: '5.0'
17 | - python-version: '3.9'
18 | django-version: '5.1'
19 | - python-version: '3.9'
20 | django-version: '5.2'
21 | - python-version: '3.9'
22 | django-version: 'main'
23 | - python-version: 'pypy-3.10'
24 | django-version: '4.1'
25 | - python-version: 'pypy-3.10'
26 | django-version: '4.2'
27 | - python-version: 'pypy-3.10'
28 | django-version: '5.0'
29 | - python-version: 'pypy-3.10'
30 | django-version: '5.1'
31 | - python-version: 'pypy-3.10'
32 | django-version: '5.2'
33 | - python-version: 'pypy-3.10'
34 | django-version: 'main'
35 | - python-version: '3.12'
36 | django-version: '4.1'
37 | - python-version: '3.13'
38 | django-version: '4.1'
39 | - python-version: '3.13'
40 | django-version: '4.2'
41 | - python-version: '3.13'
42 | django-version: '5.0'
43 | - python-version: '3.13'
44 | django-version: '5.1'
45 |
46 | steps:
47 | - uses: actions/checkout@v4
48 | - uses: actions/setup-java@v4
49 | with:
50 | distribution: 'temurin'
51 | java-version: '21'
52 |
53 | - name: Set up Python ${{ matrix.python-version }}
54 | uses: actions/setup-python@v5
55 | with:
56 | python-version: ${{ matrix.python-version }}
57 |
58 | - name: Set up Node
59 | uses: actions/setup-node@v4
60 | with:
61 | node-version: '16'
62 |
63 | - name: Install Node dependencies
64 | run: npm install
65 |
66 | - name: Get pip cache dir
67 | id: pip-cache
68 | run: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
69 |
70 | - name: Cache
71 | uses: actions/cache@v4
72 | with:
73 | path: ${{ steps.pip-cache.outputs.dir }}
74 | key:
75 | ${{ matrix.python-version }}-v1-${{ hashFiles('**/pyproject.toml') }}-${{ hashFiles('**/tox.ini') }}
76 | restore-keys: |
77 | ${{ matrix.python-version }}-v1-
78 |
79 | - name: Install Python dependencies
80 | run: |
81 | python -m pip install --upgrade pip
82 | python -m pip install --upgrade tox tox-gh-actions
83 |
84 | - name: Tox tests
85 | run: |
86 | tox -v
87 | env:
88 | DJANGO: ${{ matrix.django-version }}
89 |
90 | - name: Upload coverage
91 | uses: codecov/codecov-action@v5
92 | with:
93 | name: Python ${{ matrix.python-version }}
94 |
95 | ruff:
96 | runs-on: ubuntu-latest
97 | steps:
98 | - uses: actions/checkout@v4
99 | - run: pip install --user ruff==0.12.5
100 | - run: ruff check . --extend-select=C4,C9,I,PLC,PLE,PLR,U --ignore=C414,I001,PLR0913,UP007,UP032 --target-version=py39
101 |
--------------------------------------------------------------------------------
/docs/templates.rst:
--------------------------------------------------------------------------------
1 | .. _ref-templates:
2 |
3 | ====================
4 | Javascript Templates
5 | ====================
6 |
7 | Pipeline allows you to use javascript templates along with your javascript views.
8 | To use your javascript templates, just add them to your ``JAVASCRIPT`` group ::
9 |
10 | PIPELINE['JAVASCRIPT'] = {
11 | 'application': {
12 | 'source_filenames': (
13 | 'js/application.js',
14 | 'js/templates/**/*.jst',
15 | ),
16 | 'output_filename': 'js/application.js'
17 | }
18 | }
19 |
20 | For example, if you have the following template ``js/templates/photo/detail.jst`` ::
21 |
22 |
23 |

24 |
25 | <%= caption %>
26 |
27 |
28 |
29 | It will be available from your javascript code via window.JST ::
30 |
31 | JST.photo_detail({ src:"images/baby-panda.jpg", caption:"A baby panda is born" });
32 |
33 |
34 | Configuration
35 | -------------
36 |
37 | Template function
38 | .................
39 |
40 | By default, Pipeline uses a variant of `Micro Templating `_ to compile the templates, but you can choose your preferred JavaScript templating engine by changing ``PIPELINE['TEMPLATE_FUNC']`` ::
41 |
42 | PIPELINE['TEMPLATE_FUNC'] = 'template'
43 |
44 | Template namespace
45 | ..................
46 |
47 | Your templates are made available in a top-level object, by default ``window.JST``,
48 | but you can choose your own via ``PIPELINE['TEMPLATE_NAMESPACE']`` ::
49 |
50 | PIPELINE['TEMPLATE_NAMESPACE'] = 'window.Template'
51 |
52 |
53 | Template extension
54 | ..................
55 |
56 | Templates are detected by their extension, by default ``.jst``, but you can use
57 | your own extension via ``PIPELINE['TEMPLATE_EXT']`` ::
58 |
59 | PIPELINE['TEMPLATE_EXT'] = '.mustache'
60 |
61 | Template separator
62 | ..................
63 |
64 | Templates identifier are built using a replacement for directory separator,
65 | by default ``_``, but you specify your own separator via ``PIPELINE['TEMPLATE_SEPARATOR']`` ::
66 |
67 | PIPELINE['TEMPLATE_SEPARATOR'] = '/'
68 |
69 |
70 | Using it with your favorite template library
71 | --------------------------------------------
72 |
73 | Mustache
74 | ........
75 |
76 | To use it with `Mustache `_ you will need
77 | some extra javascript ::
78 |
79 | Mustache.template = function(templateString) {
80 | return function() {
81 | if (arguments.length < 1) {
82 | return templateString;
83 | } else {
84 | return Mustache.to_html(templateString, arguments[0], arguments[1]);
85 | }
86 | };
87 | };
88 |
89 | And use these settings ::
90 |
91 | PIPELINE['TEMPLATE_EXT'] = '.mustache'
92 | PIPELINE['TEMPLATE_FUNC'] = 'Mustache.template'
93 |
94 | Handlebars
95 | ..........
96 |
97 | To use it with `Handlebars `_, use the following settings ::
98 |
99 | PIPELINE['TEMPLATE_EXT'] = '.handlebars'
100 | PIPELINE['TEMPLATE_FUNC'] = 'Handlebars.compile'
101 | PIPELINE['TEMPLATE_NAMESPACE'] = 'Handlebars.templates'
102 |
103 | Ember.js + Handlebars
104 | .....................
105 |
106 | To use it with `Ember.js `_, use the following settings ::
107 |
108 | PIPELINE['TEMPLATE_EXT'] = '.handlebars'
109 | PIPELINE['TEMPLATE_FUNC'] = 'Ember.Handlebars.compile'
110 | PIPELINE['TEMPLATE_NAMESPACE'] = 'window.Ember.TEMPLATES'
111 | PIPELINE['TEMPLATE_SEPARATOR'] = '/'
112 |
113 | Prototype
114 | .........
115 |
116 | To use it with `Prototype `_, just setup your
117 | ``PIPELINE['TEMPLATE_FUNC']`` ::
118 |
119 | PIPELINE['TEMPLATE_FUNC'] = 'new Template'
120 |
121 |
--------------------------------------------------------------------------------
/pipeline/collector.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import django
4 | from django.contrib.staticfiles import finders
5 | from django.contrib.staticfiles.storage import staticfiles_storage
6 |
7 | from pipeline.finders import PipelineFinder
8 |
9 |
10 | class Collector:
11 | request = None
12 |
13 | def __init__(self, storage=None):
14 | if storage is None:
15 | storage = staticfiles_storage
16 | self.storage = storage
17 |
18 | def _get_modified_time(self, storage, prefixed_path):
19 | if django.VERSION[:2] >= (1, 10):
20 | return storage.get_modified_time(prefixed_path)
21 | return storage.modified_time(prefixed_path)
22 |
23 | def clear(self, path=""):
24 | dirs, files = self.storage.listdir(path)
25 | for f in files:
26 | fpath = os.path.join(path, f)
27 | self.storage.delete(fpath)
28 | for d in dirs:
29 | self.clear(os.path.join(path, d))
30 |
31 | def collect(self, request=None, files=[]):
32 | if self.request and self.request is request:
33 | return
34 | self.request = request
35 | found_files = {}
36 | for finder in finders.get_finders():
37 | # Ignore our finder to avoid looping
38 | if isinstance(finder, PipelineFinder):
39 | continue
40 | for path, storage in finder.list(["CVS", ".*", "*~"]):
41 | # Prefix the relative path if the source storage contains it
42 | if getattr(storage, "prefix", None):
43 | prefixed_path = os.path.join(storage.prefix, path)
44 | else:
45 | prefixed_path = path
46 |
47 | if prefixed_path not in found_files and (
48 | not files or prefixed_path in files
49 | ):
50 | found_files[prefixed_path] = (storage, path)
51 | self.copy_file(path, prefixed_path, storage)
52 |
53 | if files and len(files) == len(found_files):
54 | break
55 |
56 | return found_files.keys()
57 |
58 | def copy_file(self, path, prefixed_path, source_storage):
59 | # Delete the target file if needed or break
60 | if not self.delete_file(path, prefixed_path, source_storage):
61 | return
62 | # Finally start copying
63 | with source_storage.open(path) as source_file:
64 | self.storage.save(prefixed_path, source_file)
65 |
66 | def delete_file(self, path, prefixed_path, source_storage):
67 | if self.storage.exists(prefixed_path):
68 | try:
69 | # When was the target file modified last time?
70 | target_last_modified = self._get_modified_time(
71 | self.storage,
72 | prefixed_path,
73 | )
74 | except (OSError, NotImplementedError, AttributeError):
75 | # The storage doesn't support ``modified_time`` or failed
76 | pass
77 | else:
78 | try:
79 | # When was the source file modified last time?
80 | source_last_modified = self._get_modified_time(source_storage, path)
81 | except (OSError, NotImplementedError, AttributeError):
82 | pass
83 | else:
84 | # Skip the file if the source file is younger
85 | # Avoid sub-second precision
86 | if target_last_modified.replace(
87 | microsecond=0
88 | ) >= source_last_modified.replace(microsecond=0):
89 | return False
90 | # Then delete the existing file if really needed
91 | self.storage.delete(prefixed_path)
92 | return True
93 |
94 |
95 | default_collector = Collector()
96 |
--------------------------------------------------------------------------------
/pipeline/conf.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shlex
3 | from collections.abc import MutableMapping
4 |
5 | from django.conf import settings as _settings
6 | from django.core.signals import setting_changed
7 | from django.dispatch import receiver
8 |
9 | DEFAULTS = {
10 | "PIPELINE_ENABLED": not _settings.DEBUG,
11 | "PIPELINE_COLLECTOR_ENABLED": True,
12 | "PIPELINE_ROOT": _settings.STATIC_ROOT,
13 | "PIPELINE_URL": _settings.STATIC_URL,
14 | "SHOW_ERRORS_INLINE": _settings.DEBUG,
15 | "CSS_COMPRESSOR": "pipeline.compressors.yuglify.YuglifyCompressor",
16 | "JS_COMPRESSOR": "pipeline.compressors.yuglify.YuglifyCompressor",
17 | "COMPILERS": [],
18 | "STYLESHEETS": {},
19 | "JAVASCRIPT": {},
20 | "TEMPLATE_NAMESPACE": "window.JST",
21 | "TEMPLATE_EXT": ".jst",
22 | "TEMPLATE_FUNC": "template",
23 | "TEMPLATE_SEPARATOR": "_",
24 | "DISABLE_WRAPPER": False,
25 | "JS_WRAPPER": "(function() {\n%s\n}).call(this);",
26 | "CSSTIDY_BINARY": "/usr/bin/env csstidy",
27 | "CSSTIDY_ARGUMENTS": "--template=highest",
28 | "YUGLIFY_BINARY": "/usr/bin/env yuglify",
29 | "YUGLIFY_CSS_ARGUMENTS": "--terminal",
30 | "YUGLIFY_JS_ARGUMENTS": "--terminal",
31 | "YUI_BINARY": "/usr/bin/env yuicompressor",
32 | "YUI_CSS_ARGUMENTS": "",
33 | "YUI_JS_ARGUMENTS": "",
34 | "CLOSURE_BINARY": "/usr/bin/env closure",
35 | "CLOSURE_ARGUMENTS": "",
36 | "UGLIFYJS_BINARY": "/usr/bin/env uglifyjs",
37 | "UGLIFYJS_ARGUMENTS": "",
38 | "TERSER_BINARY": "/usr/bin/env terser",
39 | "TERSER_ARGUMENTS": "--compress",
40 | "CSSMIN_BINARY": "/usr/bin/env cssmin",
41 | "CSSMIN_ARGUMENTS": "",
42 | "COFFEE_SCRIPT_BINARY": "/usr/bin/env coffee",
43 | "COFFEE_SCRIPT_ARGUMENTS": "",
44 | "BABEL_BINARY": "/usr/bin/env babel",
45 | "BABEL_ARGUMENTS": "",
46 | "LIVE_SCRIPT_BINARY": "/usr/bin/env lsc",
47 | "LIVE_SCRIPT_ARGUMENTS": "",
48 | "TYPE_SCRIPT_BINARY": "/usr/bin/env tsc",
49 | "TYPE_SCRIPT_ARGUMENTS": "",
50 | "SASS_BINARY": "/usr/bin/env sass",
51 | "SASS_ARGUMENTS": "",
52 | "STYLUS_BINARY": "/usr/bin/env stylus",
53 | "STYLUS_ARGUMENTS": "",
54 | "LESS_BINARY": "/usr/bin/env lessc",
55 | "LESS_ARGUMENTS": "",
56 | "MIMETYPES": (
57 | (("text/coffeescript"), (".coffee")),
58 | (("text/less"), (".less")),
59 | (("text/javascript"), (".js")),
60 | (("text/typescript"), (".ts")),
61 | (("text/x-sass"), (".sass")),
62 | (("text/x-scss"), (".scss")),
63 | ),
64 | "EMBED_MAX_IMAGE_SIZE": 32700,
65 | "EMBED_PATH": r"[/]?embed/",
66 | }
67 |
68 |
69 | class PipelineSettings(MutableMapping):
70 | """
71 | Container object for pipeline settings
72 | """
73 |
74 | def __init__(self, wrapped_settings):
75 | self.settings = DEFAULTS.copy()
76 | self.settings.update(wrapped_settings)
77 |
78 | def __getitem__(self, key):
79 | value = self.settings[key]
80 | if key.endswith(("_BINARY", "_ARGUMENTS")):
81 | if isinstance(value, (str,)):
82 | return tuple(shlex.split(value, posix=(os.name == "posix")))
83 | return tuple(value)
84 | return value
85 |
86 | def __setitem__(self, key, value):
87 | self.settings[key] = value
88 |
89 | def __delitem__(self, key):
90 | del self.store[key]
91 |
92 | def __iter__(self):
93 | return iter(self.settings)
94 |
95 | def __len__(self):
96 | return len(self.settings)
97 |
98 | def __getattr__(self, name):
99 | return self.__getitem__(name)
100 |
101 |
102 | settings = PipelineSettings(_settings.PIPELINE)
103 |
104 |
105 | @receiver(setting_changed)
106 | def reload_settings(**kwargs):
107 | if kwargs["setting"] == "PIPELINE":
108 | settings.update(kwargs["value"])
109 |
--------------------------------------------------------------------------------
/pipeline/finders.py:
--------------------------------------------------------------------------------
1 | from itertools import chain
2 | from os.path import normpath
3 |
4 | from django.contrib.staticfiles.finders import (
5 | AppDirectoriesFinder as DjangoAppDirectoriesFinder,
6 | )
7 | from django.contrib.staticfiles.finders import BaseFinder, BaseStorageFinder
8 | from django.contrib.staticfiles.finders import (
9 | FileSystemFinder as DjangoFileSystemFinder,
10 | )
11 | from django.contrib.staticfiles.finders import find
12 | from django.contrib.staticfiles.storage import staticfiles_storage
13 | from django.utils._os import safe_join
14 |
15 | from pipeline.conf import settings
16 |
17 |
18 | class PipelineFinder(BaseStorageFinder):
19 | storage = staticfiles_storage
20 |
21 | def find(self, path, **kwargs):
22 | if not settings.PIPELINE_ENABLED:
23 | return super().find(path, **kwargs)
24 | else:
25 | return []
26 |
27 | def list(self, ignore_patterns):
28 | return []
29 |
30 |
31 | class ManifestFinder(BaseFinder):
32 | def find(self, path, **kwargs):
33 | """
34 | Looks for files in PIPELINE.STYLESHEETS and PIPELINE.JAVASCRIPT
35 | """
36 | matches = []
37 | for elem in chain(settings.STYLESHEETS.values(), settings.JAVASCRIPT.values()):
38 | if normpath(elem["output_filename"]) == normpath(path):
39 | match = safe_join(settings.PIPELINE_ROOT, path)
40 | if not kwargs.get("find_all", kwargs.get("all", False)):
41 | return match
42 | matches.append(match)
43 | return matches
44 |
45 | def list(self, *args):
46 | return []
47 |
48 |
49 | class CachedFileFinder(BaseFinder):
50 | def find(self, path, **kwargs):
51 | """
52 | Work out the uncached name of the file and look that up instead
53 | """
54 | try:
55 | start, _, extn = path.rsplit(".", 2)
56 | except ValueError:
57 | return []
58 | path = ".".join((start, extn))
59 | return find(path, **kwargs) or []
60 |
61 | def list(self, *args):
62 | return []
63 |
64 |
65 | class PatternFilterMixin:
66 | ignore_patterns = []
67 |
68 | def get_ignored_patterns(self):
69 | return list(set(self.ignore_patterns))
70 |
71 | def list(self, ignore_patterns):
72 | if ignore_patterns:
73 | ignore_patterns = ignore_patterns + self.get_ignored_patterns()
74 | return super().list(ignore_patterns)
75 |
76 |
77 | class AppDirectoriesFinder(PatternFilterMixin, DjangoAppDirectoriesFinder):
78 | """
79 | Like AppDirectoriesFinder, but doesn't return any additional ignored
80 | patterns.
81 |
82 | This allows us to concentrate/compress our components without dragging
83 | the raw versions in via collectstatic.
84 | """
85 |
86 | ignore_patterns = [
87 | "*.js",
88 | "*.css",
89 | "*.less",
90 | "*.scss",
91 | "*.styl",
92 | ]
93 |
94 |
95 | class FileSystemFinder(PatternFilterMixin, DjangoFileSystemFinder):
96 | """
97 | Like FileSystemFinder, but doesn't return any additional ignored patterns
98 |
99 | This allows us to concentrate/compress our components without dragging
100 | the raw versions in too.
101 | """
102 |
103 | ignore_patterns = [
104 | "*.js",
105 | "*.css",
106 | "*.less",
107 | "*.scss",
108 | "*.styl",
109 | "*.sh",
110 | "*.html",
111 | "*.md",
112 | "*.markdown",
113 | "*.php",
114 | "*.txt",
115 | "README*",
116 | "LICENSE*",
117 | "*examples*",
118 | "*test*",
119 | "*bin*",
120 | "*samples*",
121 | "*docs*",
122 | "*build*",
123 | "*demo*",
124 | "Makefile*",
125 | "Gemfile*",
126 | "node_modules",
127 | ]
128 |
--------------------------------------------------------------------------------
/tests/tests/test_glob.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 |
4 | from django.core.files.base import ContentFile
5 | from django.core.files.storage import FileSystemStorage
6 | from django.test import TestCase
7 |
8 | from pipeline import glob
9 |
10 |
11 | def local_path(path):
12 | return os.path.join(os.path.dirname(__file__), path)
13 |
14 |
15 | class GlobTest(TestCase):
16 | def normpath(self, *parts):
17 | return os.path.normpath(os.path.join(*parts))
18 |
19 | def mktemp(self, *parts):
20 | filename = self.normpath(*parts)
21 | base, file = os.path.split(filename)
22 | base = os.path.join(self.storage.location, base)
23 | if not os.path.exists(base):
24 | os.makedirs(base)
25 | self.storage.save(filename, ContentFile(""))
26 |
27 | def assertSequenceEqual(self, l1, l2):
28 | self.assertEqual(set(l1), set(l2))
29 |
30 | def setUp(self):
31 | self.storage = FileSystemStorage(local_path("glob_dir"))
32 | self.old_storage = glob.staticfiles_storage
33 | glob.staticfiles_storage = self.storage
34 | self.mktemp("a", "D")
35 | self.mktemp("aab", "F")
36 | self.mktemp("aaa", "zzzF")
37 | self.mktemp("ZZZ")
38 | self.mktemp("a", "bcd", "EF")
39 | self.mktemp("a", "bcd", "efg", "ha")
40 |
41 | def glob(self, *parts):
42 | if len(parts) == 1:
43 | pattern = parts[0]
44 | else:
45 | pattern = os.path.join(*parts)
46 | return glob.glob(pattern)
47 |
48 | def tearDown(self):
49 | shutil.rmtree(self.storage.location)
50 | glob.staticfiles_storage = self.old_storage
51 |
52 | def test_glob_literal(self):
53 | self.assertSequenceEqual(self.glob("a"), [self.normpath("a")])
54 | self.assertSequenceEqual(self.glob("a", "D"), [self.normpath("a", "D")])
55 | self.assertSequenceEqual(self.glob("aab"), [self.normpath("aab")])
56 |
57 | def test_glob_one_directory(self):
58 | self.assertSequenceEqual(
59 | self.glob("a*"), map(self.normpath, ["a", "aab", "aaa"])
60 | )
61 | self.assertSequenceEqual(self.glob("*a"), map(self.normpath, ["a", "aaa"]))
62 | self.assertSequenceEqual(self.glob("aa?"), map(self.normpath, ["aaa", "aab"]))
63 | self.assertSequenceEqual(
64 | self.glob("aa[ab]"), map(self.normpath, ["aaa", "aab"])
65 | )
66 | self.assertSequenceEqual(self.glob("*q"), [])
67 |
68 | def test_glob_nested_directory(self):
69 | if os.path.normcase("abCD") == "abCD":
70 | # case-sensitive filesystem
71 | self.assertSequenceEqual(
72 | self.glob("a", "bcd", "E*"), [self.normpath("a", "bcd", "EF")]
73 | )
74 | else:
75 | # case insensitive filesystem
76 | self.assertSequenceEqual(
77 | self.glob("a", "bcd", "E*"),
78 | [self.normpath("a", "bcd", "EF"), self.normpath("a", "bcd", "efg")],
79 | )
80 | self.assertSequenceEqual(
81 | self.glob("a", "bcd", "*g"), [self.normpath("a", "bcd", "efg")]
82 | )
83 |
84 | def test_glob_directory_names(self):
85 | self.assertSequenceEqual(self.glob("*", "D"), [self.normpath("a", "D")])
86 | self.assertSequenceEqual(self.glob("*", "*a"), [])
87 | self.assertSequenceEqual(
88 | self.glob("a", "*", "*", "*a"), [self.normpath("a", "bcd", "efg", "ha")]
89 | )
90 | self.assertSequenceEqual(
91 | self.glob("?a?", "*F"),
92 | map(self.normpath, [os.path.join("aaa", "zzzF"), os.path.join("aab", "F")]),
93 | )
94 |
95 | def test_glob_directory_with_trailing_slash(self):
96 | # We are verifying that when there is wildcard pattern which
97 | # ends with os.sep doesn't blow up.
98 | paths = glob.glob("*" + os.sep)
99 | self.assertEqual(len(paths), 4)
100 | self.assertTrue(all(os.sep in path for path in paths))
101 |
--------------------------------------------------------------------------------
/pipeline/storage.py:
--------------------------------------------------------------------------------
1 | import gzip
2 | from io import BytesIO
3 |
4 | from django import get_version as django_version
5 | from django.contrib.staticfiles.storage import (
6 | ManifestStaticFilesStorage,
7 | StaticFilesStorage,
8 | )
9 | from django.contrib.staticfiles.utils import matches_patterns
10 | from django.core.files.base import File
11 |
12 | _CACHED_STATIC_FILES_STORAGE_AVAILABLE = django_version() < "3.1"
13 |
14 | if _CACHED_STATIC_FILES_STORAGE_AVAILABLE:
15 | from django.contrib.staticfiles.storage import CachedStaticFilesStorage
16 |
17 |
18 | class PipelineMixin:
19 | packing = True
20 |
21 | def post_process(self, paths, dry_run=False, **options):
22 | if dry_run:
23 | return
24 |
25 | from pipeline.packager import Packager # noqa: PLC0415
26 |
27 | packager = Packager(storage=self)
28 | for package_name in packager.packages["css"]:
29 | package = packager.package_for("css", package_name)
30 | output_file = package.output_filename
31 | if self.packing:
32 | packager.pack_stylesheets(package)
33 | paths[output_file] = (self, output_file)
34 | yield output_file, output_file, True
35 | for package_name in packager.packages["js"]:
36 | package = packager.package_for("js", package_name)
37 | output_file = package.output_filename
38 | if self.packing:
39 | packager.pack_javascripts(package)
40 | paths[output_file] = (self, output_file)
41 | yield output_file, output_file, True
42 |
43 | super_class = super()
44 | if hasattr(super_class, "post_process"):
45 | yield from super_class.post_process(paths.copy(), dry_run, **options)
46 |
47 | def get_available_name(self, name, max_length=None):
48 | if self.exists(name):
49 | self.delete(name)
50 | return name
51 |
52 |
53 | class GZIPMixin:
54 | gzip_patterns = ("*.css", "*.js")
55 |
56 | def _compress(self, original_file):
57 | content = BytesIO()
58 | gzip_file = gzip.GzipFile(mode="wb", fileobj=content)
59 | gzip_file.write(original_file.read())
60 | gzip_file.close()
61 | content.seek(0)
62 | return File(content)
63 |
64 | def post_process(self, paths, dry_run=False, **options):
65 | super_class = super()
66 | if hasattr(super_class, "post_process"):
67 | for name, hashed_name, processed in super_class.post_process(
68 | paths.copy(), dry_run, **options
69 | ):
70 | if hashed_name != name:
71 | paths[hashed_name] = (self, hashed_name)
72 | yield name, hashed_name, processed
73 |
74 | if dry_run:
75 | return
76 |
77 | for path in paths:
78 | if path:
79 | if not matches_patterns(path, self.gzip_patterns):
80 | continue
81 | original_file = self.open(path)
82 | gzipped_path = f"{path}.gz"
83 | if self.exists(gzipped_path):
84 | self.delete(gzipped_path)
85 | gzipped_file = self._compress(original_file)
86 | gzipped_path = self.save(gzipped_path, gzipped_file)
87 | yield gzipped_path, gzipped_path, True
88 |
89 |
90 | class NonPackagingMixin:
91 | packing = False
92 |
93 |
94 | class PipelineStorage(PipelineMixin, StaticFilesStorage):
95 | pass
96 |
97 |
98 | class NonPackagingPipelineStorage(NonPackagingMixin, PipelineStorage):
99 | pass
100 |
101 |
102 | if _CACHED_STATIC_FILES_STORAGE_AVAILABLE:
103 |
104 | class PipelineCachedStorage(PipelineMixin, CachedStaticFilesStorage):
105 | # Deprecated since Django 2.2
106 | # Removed in Django 3.1
107 | pass
108 |
109 | class NonPackagingPipelineCachedStorage(NonPackagingMixin, PipelineCachedStorage):
110 | # Deprecated since Django 2.2
111 | # Removed in Django 3.1
112 | pass
113 |
114 |
115 | class PipelineManifestStorage(PipelineMixin, ManifestStaticFilesStorage):
116 | pass
117 |
118 |
119 | class NonPackagingPipelineManifestStorage(
120 | NonPackagingMixin, ManifestStaticFilesStorage
121 | ):
122 | pass
123 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Pipeline
2 | ========
3 |
4 | .. image:: https://jazzband.co/static/img/badge.svg
5 | :alt: Jazzband
6 | :target: https://jazzband.co/
7 |
8 | .. image:: https://github.com/jazzband/django-pipeline/workflows/Test/badge.svg
9 | :target: https://github.com/jazzband/django-pipeline/actions
10 | :alt: GitHub Actions
11 |
12 | .. image:: https://codecov.io/gh/jazzband/django-pipeline/branch/master/graph/badge.svg
13 | :target: https://codecov.io/gh/jazzband/django-pipeline
14 | :alt: Coverage
15 |
16 | .. image:: https://readthedocs.org/projects/django-pipeline/badge/?version=latest
17 | :alt: Documentation Status
18 | :target: https://django-pipeline.readthedocs.io/en/latest/?badge=latest
19 |
20 |
21 | Pipeline is an asset packaging library for Django, providing both CSS and
22 | JavaScript concatenation and compression, built-in JavaScript template support,
23 | and optional data-URI image and font embedding.
24 |
25 | .. image:: https://github.com/jazzband/django-pipeline/raw/master/img/django-pipeline.svg
26 | :alt: Django Pipeline Overview
27 |
28 |
29 | Installation
30 | ------------
31 |
32 | To install it, simply:
33 |
34 | .. code-block:: bash
35 |
36 | pip install django-pipeline
37 |
38 |
39 | Quickstart
40 | ----------
41 |
42 | Pipeline compiles and compress your assets files from
43 | ``STATICFILES_DIRS`` to your ``STATIC_ROOT`` when you run Django's
44 | ``collectstatic`` command.
45 |
46 | These simple steps add Pipeline to your project to compile multiple ``.js`` and
47 | ``.css`` file into one and compress them.
48 |
49 | Add Pipeline to your installed apps:
50 |
51 | .. code-block:: python
52 |
53 | # settings.py
54 | INSTALLED_APPS = [
55 | ...
56 | 'pipeline',
57 | ]
58 |
59 |
60 | Use Pipeline specified classes for ``STATICFILES_FINDERS`` and ``STATICFILES_STORAGE``:
61 |
62 | .. code-block:: python
63 |
64 | STATICFILES_STORAGE = 'pipeline.storage.PipelineManifestStorage'
65 |
66 | STATICFILES_FINDERS = (
67 | 'django.contrib.staticfiles.finders.FileSystemFinder',
68 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
69 | 'pipeline.finders.PipelineFinder',
70 | )
71 |
72 |
73 | Configure Pipeline:
74 |
75 | .. code-block:: python
76 |
77 | # The folowing config merges CSS files(main.css, normalize.css)
78 | # and JavaScript files(app.js, script.js) and compress them using
79 | # `yuglify` into `css/styles.css` and `js/main.js`
80 | # NOTE: Pipeline only works when DEBUG is False
81 | PIPELINE = {
82 | 'STYLESHEETS': {
83 | 'css_files': {
84 | 'source_filenames': (
85 | 'css/main.css',
86 | 'css/normalize.css',
87 | ),
88 | 'output_filename': 'css/styles.css',
89 | 'extra_context': {
90 | 'media': 'screen,projection',
91 | },
92 | },
93 | },
94 | 'JAVASCRIPT': {
95 | 'js_files': {
96 | 'source_filenames': (
97 | 'js/app.js',
98 | 'js/script.js',
99 | ),
100 | 'output_filename': 'js/main.js',
101 | }
102 | }
103 | }
104 |
105 |
106 | Then, you have to install compilers and compressors binary manually.
107 |
108 | For example, you can install them using `NPM `_
109 | and address them from ``node_modules`` directory in your project path:
110 |
111 | .. code-block:: python
112 |
113 | PIPELINE.update({
114 | 'YUGLIFY_BINARY': path.join(BASE_DIR, 'node_modules/.bin/yuglify'),
115 | })
116 | # For a list of all supported compilers and compressors see documentation
117 |
118 |
119 | Load static files in your template:
120 |
121 | .. code-block::
122 |
123 | {% load pipeline %}
124 | {% stylesheet 'css_files' %}
125 | {% javascript 'js_files' %}
126 |
127 |
128 | Documentation
129 | -------------
130 |
131 | For documentation, usage, and examples, see:
132 | https://django-pipeline.readthedocs.io
133 |
134 |
135 | Issues
136 | ------
137 | You can report bugs and discuss features on the `issues page `_.
138 |
139 |
140 | Changelog
141 | ---------
142 |
143 | See `HISTORY.rst `_.
144 |
--------------------------------------------------------------------------------
/docs/storages.rst:
--------------------------------------------------------------------------------
1 | .. _ref-storages:
2 |
3 | ========
4 | Storages
5 | ========
6 |
7 |
8 | Using with staticfiles
9 | ======================
10 |
11 | Pipeline is providing a storage for `staticfiles app `_,
12 | to use it configure ``STATICFILES_STORAGE`` like so ::
13 |
14 | STATICFILES_STORAGE = 'pipeline.storage.PipelineStorage'
15 |
16 | And if you want versioning use ::
17 |
18 | STATICFILES_STORAGE = 'pipeline.storage.PipelineManifestStorage'
19 |
20 | There is also non-packing storage available, that allows you to run ``collectstatic`` command
21 | without packaging your assets. Useful for production when you don't want to run compressor or compilers ::
22 |
23 | STATICFILES_STORAGE = 'pipeline.storage.NonPackagingPipelineStorage'
24 |
25 | Also available if you want versioning ::
26 |
27 | STATICFILES_STORAGE = 'pipeline.storage.NonPackagingPipelineManifestStorage'
28 |
29 | If you use staticfiles with ``DEBUG = False`` (i.e. for integration tests
30 | with `Selenium `_) you should install the finder
31 | that allows staticfiles to locate your outputted assets : ::
32 |
33 | STATICFILES_FINDERS = (
34 | 'django.contrib.staticfiles.finders.FileSystemFinder',
35 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
36 | 'pipeline.finders.PipelineFinder',
37 | )
38 |
39 | If you use ``PipelineCachedStorage`` you may also like the ``CachedFileFinder``,
40 | which allows you to use integration tests with cached file URLs.
41 |
42 | Keep in mind that ``PipelineCachedStorage`` is only available for Django versions
43 | before 3.1.
44 |
45 | If you want to exclude Pipelinable content from your collected static files,
46 | you can also use Pipeline's ``FileSystemFinder`` and ``AppDirectoriesFinder``.
47 | These finders will also exclude `unwanted` content like READMEs, tests and
48 | examples, which are particularly useful if you're collecting content from a
49 | tool like Bower. ::
50 |
51 | STATICFILES_FINDERS = (
52 | 'pipeline.finders.FileSystemFinder',
53 | 'pipeline.finders.AppDirectoriesFinder',
54 | 'pipeline.finders.CachedFileFinder',
55 | 'pipeline.finders.PipelineFinder',
56 | )
57 |
58 | GZIP compression
59 | ================
60 |
61 | Pipeline can also creates a gzipped version of your collected static files,
62 | so that you can avoid compressing them on the fly. ::
63 |
64 | STATICFILES_STORAGE = 'your.app.GZIPCachedStorage'
65 |
66 | The storage need to inherit from ``GZIPMixin``: ::
67 |
68 | from django.contrib.staticfiles.storage import CachedStaticFilesStorage
69 |
70 | from pipeline.storage import GZIPMixin
71 |
72 |
73 | class GZIPCachedStorage(GZIPMixin, CachedStaticFilesStorage):
74 | pass
75 |
76 |
77 | Using with other storages
78 | =========================
79 |
80 | You can also use your own custom storage, for example, if you want to use S3 for your assets : ::
81 |
82 | STATICFILES_STORAGE = 'your.app.S3PipelineManifestStorage'
83 |
84 | Your storage only needs to inherit from ``PipelineMixin`` and ``ManifestFilesMixin`` or ``CachedFilesMixin``.
85 |
86 | In Django 1.7+ you should use `ManifestFilesMixin `_
87 | unless you don't have access to the local filesystem in which case you should use ``CachedFilesMixin``. ::
88 |
89 | from django.contrib.staticfiles.storage import CachedFilesMixin, ManifestFilesMixin
90 |
91 | from pipeline.storage import PipelineMixin
92 |
93 | from storages.backends.s3boto import S3BotoStorage
94 |
95 | class S3PipelineManifestStorage(PipelineMixin, ManifestFilesMixin, S3BotoStorage):
96 | pass
97 |
98 | class S3PipelineCachedStorage(PipelineMixin, CachedFilesMixin, S3BotoStorage):
99 | pass
100 |
101 |
102 | Using Pipeline with Bower
103 | =========================
104 |
105 | `Bower `_ is a `package manager for the web` that allows
106 | you to easily include frontend components with named versions. Integrating
107 | Bower with Pipeline is straightforward.
108 |
109 | Add your Bower directory to your ``STATICFILES_DIRS`` : ::
110 |
111 | STATICFILES_DIRS = (
112 | os.path.join(os.path.dirname(__file__), '..', 'bower_components'),
113 | )
114 |
115 | Then process the relevant content through Pipeline : ::
116 |
117 | PIPELINE['JAVASCRIPT'] = {
118 | 'components': {
119 | 'source_filenames': (
120 | 'jquery/jquery.js',
121 | # you can choose to be specific to reduce your payload
122 | 'jquery-ui/ui/*.js',
123 | ),
124 | 'output_filename': 'js/components.js',
125 | },
126 | }
127 |
128 | ``pipeline.finders.FileSystemFinder`` will help you by excluding much of the
129 | extra content that Bower includes with its components, such as READMEs, tests
130 | and examples, while still including images, fonts, CSS fragments etc.
131 |
--------------------------------------------------------------------------------
/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 |
15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
16 |
17 | help:
18 | @echo "Please use \`make ' where is one of"
19 | @echo " html to make standalone HTML files"
20 | @echo " dirhtml to make HTML files named index.html in directories"
21 | @echo " singlehtml to make a single large HTML file"
22 | @echo " pickle to make pickle files"
23 | @echo " json to make JSON files"
24 | @echo " htmlhelp to make HTML files and a HTML help project"
25 | @echo " qthelp to make HTML files and a qthelp project"
26 | @echo " devhelp to make HTML files and a Devhelp project"
27 | @echo " epub to make an epub"
28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
29 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
30 | @echo " text to make text files"
31 | @echo " man to make manual pages"
32 | @echo " changes to make an overview of all changed/added/deprecated items"
33 | @echo " linkcheck to check all external links for integrity"
34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
35 |
36 | clean:
37 | -rm -rf $(BUILDDIR)/*
38 |
39 | html:
40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
41 | @echo
42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
43 |
44 | dirhtml:
45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
46 | @echo
47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
48 |
49 | singlehtml:
50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
51 | @echo
52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
53 |
54 | pickle:
55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
56 | @echo
57 | @echo "Build finished; now you can process the pickle files."
58 |
59 | json:
60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
61 | @echo
62 | @echo "Build finished; now you can process the JSON files."
63 |
64 | htmlhelp:
65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
66 | @echo
67 | @echo "Build finished; now you can run HTML Help Workshop with the" \
68 | ".hhp project file in $(BUILDDIR)/htmlhelp."
69 |
70 | qthelp:
71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
72 | @echo
73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-pipeline.qhcp"
76 | @echo "To view the help file:"
77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-pipeline.qhc"
78 |
79 | devhelp:
80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
81 | @echo
82 | @echo "Build finished."
83 | @echo "To view the help file:"
84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-pipeline"
85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-pipeline"
86 | @echo "# devhelp"
87 |
88 | epub:
89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
90 | @echo
91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
92 |
93 | latex:
94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
95 | @echo
96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
98 | "(use \`make latexpdf' here to do that automatically)."
99 |
100 | latexpdf:
101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
102 | @echo "Running LaTeX files through pdflatex..."
103 | make -C $(BUILDDIR)/latex all-pdf
104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
105 |
106 | text:
107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
108 | @echo
109 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
110 |
111 | man:
112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
113 | @echo
114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
115 |
116 | changes:
117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
118 | @echo
119 | @echo "The overview file is in $(BUILDDIR)/changes."
120 |
121 | linkcheck:
122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
123 | @echo
124 | @echo "Link check complete; look for any errors in the above output " \
125 | "or in $(BUILDDIR)/linkcheck/output.txt."
126 |
127 | doctest:
128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
129 | @echo "Testing of doctests in the sources finished, look at the " \
130 | "results in $(BUILDDIR)/doctest/output.txt."
131 |
--------------------------------------------------------------------------------
/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 | if NOT "%PAPER%" == "" (
11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
12 | )
13 |
14 | if "%1" == "" goto help
15 |
16 | if "%1" == "help" (
17 | :help
18 | echo.Please use `make ^` where ^ is one of
19 | echo. html to make standalone HTML files
20 | echo. dirhtml to make HTML files named index.html in directories
21 | echo. singlehtml to make a single large HTML file
22 | echo. pickle to make pickle files
23 | echo. json to make JSON files
24 | echo. htmlhelp to make HTML files and a HTML help project
25 | echo. qthelp to make HTML files and a qthelp project
26 | echo. devhelp to make HTML files and a Devhelp project
27 | echo. epub to make an epub
28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
29 | echo. text to make text files
30 | echo. man to make manual pages
31 | echo. changes to make an overview over all changed/added/deprecated items
32 | echo. linkcheck to check all external links for integrity
33 | echo. doctest to run all doctests embedded in the documentation if enabled
34 | goto end
35 | )
36 |
37 | if "%1" == "clean" (
38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
39 | del /q /s %BUILDDIR%\*
40 | goto end
41 | )
42 |
43 | if "%1" == "html" (
44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
45 | if errorlevel 1 exit /b 1
46 | echo.
47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
48 | goto end
49 | )
50 |
51 | if "%1" == "dirhtml" (
52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
53 | if errorlevel 1 exit /b 1
54 | echo.
55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
56 | goto end
57 | )
58 |
59 | if "%1" == "singlehtml" (
60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
61 | if errorlevel 1 exit /b 1
62 | echo.
63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
64 | goto end
65 | )
66 |
67 | if "%1" == "pickle" (
68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
69 | if errorlevel 1 exit /b 1
70 | echo.
71 | echo.Build finished; now you can process the pickle files.
72 | goto end
73 | )
74 |
75 | if "%1" == "json" (
76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
77 | if errorlevel 1 exit /b 1
78 | echo.
79 | echo.Build finished; now you can process the JSON files.
80 | goto end
81 | )
82 |
83 | if "%1" == "htmlhelp" (
84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
85 | if errorlevel 1 exit /b 1
86 | echo.
87 | echo.Build finished; now you can run HTML Help Workshop with the ^
88 | .hhp project file in %BUILDDIR%/htmlhelp.
89 | goto end
90 | )
91 |
92 | if "%1" == "qthelp" (
93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
94 | if errorlevel 1 exit /b 1
95 | echo.
96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
97 | .qhcp project file in %BUILDDIR%/qthelp, like this:
98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-pipeline.qhcp
99 | echo.To view the help file:
100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-pipeline.ghc
101 | goto end
102 | )
103 |
104 | if "%1" == "devhelp" (
105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
106 | if errorlevel 1 exit /b 1
107 | echo.
108 | echo.Build finished.
109 | goto end
110 | )
111 |
112 | if "%1" == "epub" (
113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
114 | if errorlevel 1 exit /b 1
115 | echo.
116 | echo.Build finished. The epub file is in %BUILDDIR%/epub.
117 | goto end
118 | )
119 |
120 | if "%1" == "latex" (
121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
122 | if errorlevel 1 exit /b 1
123 | echo.
124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
125 | goto end
126 | )
127 |
128 | if "%1" == "text" (
129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
130 | if errorlevel 1 exit /b 1
131 | echo.
132 | echo.Build finished. The text files are in %BUILDDIR%/text.
133 | goto end
134 | )
135 |
136 | if "%1" == "man" (
137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
138 | if errorlevel 1 exit /b 1
139 | echo.
140 | echo.Build finished. The manual pages are in %BUILDDIR%/man.
141 | goto end
142 | )
143 |
144 | if "%1" == "changes" (
145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
146 | if errorlevel 1 exit /b 1
147 | echo.
148 | echo.The overview file is in %BUILDDIR%/changes.
149 | goto end
150 | )
151 |
152 | if "%1" == "linkcheck" (
153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
154 | if errorlevel 1 exit /b 1
155 | echo.
156 | echo.Link check complete; look for any errors in the above output ^
157 | or in %BUILDDIR%/linkcheck/output.txt.
158 | goto end
159 | )
160 |
161 | if "%1" == "doctest" (
162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
163 | if errorlevel 1 exit /b 1
164 | echo.
165 | echo.Testing of doctests in the sources finished, look at the ^
166 | results in %BUILDDIR%/doctest/output.txt.
167 | goto end
168 | )
169 |
170 | :end
171 |
--------------------------------------------------------------------------------
/docs/usage.rst:
--------------------------------------------------------------------------------
1 | .. _ref-usage:
2 |
3 | =====
4 | Usage
5 | =====
6 |
7 | Describes how to use Pipeline when it is installed and configured.
8 |
9 | Templatetags
10 | ============
11 |
12 | Pipeline includes two template tags: ``stylesheet`` and ``javascript``,
13 | in a template library called ``pipeline``.
14 |
15 | They are used to output the ```` and ``', # noqa
43 | template.render(),
44 | )
45 |
46 | def test_package_js_async(self):
47 | template = self.env.from_string("""{% javascript "scripts_async" %}""")
48 | self.assertEqual(
49 | '', # noqa
50 | template.render(),
51 | )
52 |
53 | def test_package_js_defer(self):
54 | template = self.env.from_string("""{% javascript "scripts_defer" %}""")
55 | self.assertEqual(
56 | '', # noqa
57 | template.render(),
58 | )
59 |
60 | def test_package_js_async_defer(self):
61 | template = self.env.from_string("""{% javascript "scripts_async_defer" %}""")
62 | self.assertEqual(
63 | '', # noqa
64 | template.render(),
65 | )
66 |
67 |
68 | class DjangoTest(TestCase):
69 | def render_template(self, template):
70 | return Template(template).render(Context())
71 |
72 | def test_compressed_empty(self):
73 | rendered = self.render_template(
74 | """{% load pipeline %}{% stylesheet "unknow" %}""",
75 | )
76 | self.assertEqual("", rendered)
77 |
78 | def test_compressed_css(self):
79 | rendered = self.render_template(
80 | """{% load pipeline %}{% stylesheet "screen" %}""",
81 | )
82 | self.assertEqual(
83 | '', # noqa
84 | rendered,
85 | )
86 |
87 | def test_compressed_css_media(self):
88 | rendered = self.render_template(
89 | """{% load pipeline %}{% stylesheet "screen_media" %}""",
90 | )
91 | self.assertEqual(
92 | '', # noqa
93 | rendered,
94 | )
95 |
96 | def test_compressed_css_title(self):
97 | rendered = self.render_template(
98 | """{% load pipeline %}{% stylesheet "screen_title" %}""",
99 | )
100 | self.assertEqual(
101 | '', # noqa
102 | rendered,
103 | )
104 |
105 | def test_compressed_js(self):
106 | rendered = self.render_template(
107 | """{% load pipeline %}{% javascript "scripts" %}""",
108 | )
109 | self.assertEqual(
110 | '', # noqa
111 | rendered,
112 | )
113 |
114 | def test_compressed_js_async(self):
115 | rendered = self.render_template(
116 | """{% load pipeline %}{% javascript "scripts_async" %}""",
117 | )
118 | self.assertEqual(
119 | '', # noqa
120 | rendered,
121 | )
122 |
123 | def test_compressed_js_defer(self):
124 | rendered = self.render_template(
125 | """{% load pipeline %}{% javascript "scripts_defer" %}""",
126 | )
127 | self.assertEqual(
128 | '', # noqa
129 | rendered,
130 | )
131 |
132 | def test_compressed_js_async_defer(self):
133 | rendered = self.render_template(
134 | """{% load pipeline %}{% javascript "scripts_async_defer" %}""",
135 | )
136 | self.assertEqual(
137 | '', # noqa
138 | rendered,
139 | )
140 |
--------------------------------------------------------------------------------
/pipeline/packager.py:
--------------------------------------------------------------------------------
1 | from django.contrib.staticfiles.finders import find, get_finders
2 | from django.contrib.staticfiles.storage import staticfiles_storage
3 | from django.core.files.base import ContentFile
4 | from django.utils.encoding import smart_bytes
5 |
6 | from pipeline.compilers import Compiler
7 | from pipeline.compressors import Compressor
8 | from pipeline.conf import settings
9 | from pipeline.exceptions import PackageNotFound
10 | from pipeline.glob import glob
11 | from pipeline.signals import css_compressed, js_compressed
12 |
13 |
14 | class Package:
15 | def __init__(self, config):
16 | self.config = config
17 | self._sources = []
18 |
19 | @property
20 | def sources(self):
21 | if not self._sources:
22 | paths = []
23 | for pattern in self.config.get("source_filenames", []):
24 | for path in glob(pattern):
25 | if path not in paths and find(path):
26 | paths.append(str(path))
27 | self._sources = paths
28 | return self._sources
29 |
30 | @property
31 | def paths(self):
32 | return [
33 | path for path in self.sources if not path.endswith(settings.TEMPLATE_EXT)
34 | ]
35 |
36 | @property
37 | def templates(self):
38 | return [path for path in self.sources if path.endswith(settings.TEMPLATE_EXT)]
39 |
40 | @property
41 | def output_filename(self):
42 | return self.config.get("output_filename")
43 |
44 | @property
45 | def extra_context(self):
46 | return self.config.get("extra_context", {})
47 |
48 | @property
49 | def template_name(self):
50 | return self.config.get("template_name")
51 |
52 | @property
53 | def variant(self):
54 | return self.config.get("variant")
55 |
56 | @property
57 | def manifest(self):
58 | return self.config.get("manifest", True)
59 |
60 | @property
61 | def compiler_options(self):
62 | return self.config.get("compiler_options", {})
63 |
64 |
65 | class Packager:
66 | def __init__(
67 | self,
68 | storage=None,
69 | verbose=False,
70 | css_packages=None,
71 | js_packages=None,
72 | ):
73 | if storage is None:
74 | storage = staticfiles_storage
75 | self.storage = storage
76 | self.verbose = verbose
77 | self.compressor = Compressor(storage=storage, verbose=verbose)
78 | self.compiler = Compiler(storage=storage, verbose=verbose)
79 | if css_packages is None:
80 | css_packages = settings.STYLESHEETS
81 | if js_packages is None:
82 | js_packages = settings.JAVASCRIPT
83 | self.packages = {
84 | "css": self.create_packages(css_packages),
85 | "js": self.create_packages(js_packages),
86 | }
87 |
88 | def package_for(self, kind, package_name):
89 | try:
90 | return self.packages[kind][package_name]
91 | except KeyError:
92 | raise PackageNotFound(
93 | "No corresponding package for {} package name : {}".format(
94 | kind, package_name
95 | )
96 | )
97 |
98 | def individual_url(self, filename):
99 | return self.storage.url(filename)
100 |
101 | def pack_stylesheets(self, package, **kwargs):
102 | return self.pack(
103 | package,
104 | self.compressor.compress_css,
105 | css_compressed,
106 | output_filename=package.output_filename,
107 | variant=package.variant,
108 | **kwargs,
109 | )
110 |
111 | def compile(self, paths, compiler_options={}, force=False):
112 | paths = self.compiler.compile(
113 | paths,
114 | compiler_options=compiler_options,
115 | force=force,
116 | )
117 | for path in paths:
118 | if not self.storage.exists(path):
119 | if self.verbose:
120 | e = (
121 | "Compiled file '%s' cannot be "
122 | "found with packager's storage. Locating it."
123 | )
124 | print(e % path)
125 |
126 | source_storage = self.find_source_storage(path)
127 | if source_storage is not None:
128 | with source_storage.open(path) as source_file:
129 | if self.verbose:
130 | print(f"Saving: {path}")
131 | self.storage.save(path, source_file)
132 | else:
133 | raise OSError(f"File does not exist: {path}")
134 | return paths
135 |
136 | def pack(self, package, compress, signal, **kwargs):
137 | output_filename = package.output_filename
138 | if self.verbose:
139 | print(f"Saving: {output_filename}")
140 | paths = self.compile(
141 | package.paths,
142 | compiler_options=package.compiler_options,
143 | force=True,
144 | )
145 | content = compress(paths, **kwargs)
146 | self.save_file(output_filename, content)
147 | signal.send(sender=self, package=package, **kwargs)
148 | return output_filename
149 |
150 | def pack_javascripts(self, package, **kwargs):
151 | return self.pack(
152 | package,
153 | self.compressor.compress_js,
154 | js_compressed,
155 | output_filename=package.output_filename,
156 | templates=package.templates,
157 | **kwargs,
158 | )
159 |
160 | def pack_templates(self, package):
161 | return self.compressor.compile_templates(package.templates)
162 |
163 | def save_file(self, path, content):
164 | return self.storage.save(path, ContentFile(smart_bytes(content)))
165 |
166 | def find_source_storage(self, path):
167 | for finder in get_finders():
168 | for short_path, storage in finder.list(""):
169 | if short_path == path:
170 | if self.verbose:
171 | print(f"Found storage: {str(self.storage)}")
172 | return storage
173 | return None
174 |
175 | def create_packages(self, config):
176 | packages = {}
177 | if not config:
178 | return packages
179 | for name in config:
180 | packages[name] = Package(config[name])
181 | return packages
182 |
--------------------------------------------------------------------------------
/pipeline/compilers/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | import subprocess
4 | from tempfile import NamedTemporaryFile
5 |
6 | from django.contrib.staticfiles import finders
7 | from django.contrib.staticfiles.storage import staticfiles_storage
8 | from django.core.files.base import ContentFile
9 | from django.utils.encoding import force_str
10 |
11 | from pipeline.conf import settings
12 | from pipeline.exceptions import CompilerError
13 | from pipeline.utils import set_std_streams_blocking, to_class
14 |
15 |
16 | class Compiler:
17 | def __init__(self, storage=None, verbose=False):
18 | if storage is None:
19 | storage = staticfiles_storage
20 | self.storage = storage
21 | self.verbose = verbose
22 |
23 | @property
24 | def compilers(self):
25 | return [to_class(compiler) for compiler in settings.COMPILERS]
26 |
27 | def compile(self, paths, compiler_options={}, force=False):
28 | def _compile(input_path):
29 | for compiler in self.compilers:
30 | compiler = compiler(verbose=self.verbose, storage=self.storage)
31 | if compiler.match_file(input_path):
32 | try:
33 | infile = self.storage.path(input_path)
34 | except NotImplementedError:
35 | infile = finders.find(input_path)
36 | project_infile = finders.find(input_path)
37 | outfile = compiler.output_path(infile, compiler.output_extension)
38 | outdated = compiler.is_outdated(project_infile, outfile)
39 | compiler.compile_file(
40 | project_infile,
41 | outfile,
42 | outdated=outdated,
43 | force=force,
44 | **compiler_options,
45 | )
46 |
47 | return compiler.output_path(input_path, compiler.output_extension)
48 | else:
49 | return input_path
50 |
51 | try:
52 | import multiprocessing # noqa: PLC0415
53 | from concurrent import futures # noqa: PLC0415
54 | except ImportError:
55 | return list(map(_compile, paths))
56 | else:
57 | with futures.ThreadPoolExecutor(
58 | max_workers=multiprocessing.cpu_count()
59 | ) as executor:
60 | return list(executor.map(_compile, paths))
61 |
62 |
63 | class CompilerBase:
64 | def __init__(self, verbose, storage):
65 | self.verbose = verbose
66 | self.storage = storage
67 |
68 | def match_file(self, filename):
69 | raise NotImplementedError
70 |
71 | def compile_file(self, infile, outfile, outdated=False, force=False):
72 | raise NotImplementedError
73 |
74 | def save_file(self, path, content):
75 | return self.storage.save(path, ContentFile(content))
76 |
77 | def read_file(self, path):
78 | file = self.storage.open(path, "rb")
79 | content = file.read()
80 | file.close()
81 | return content
82 |
83 | def output_path(self, path, extension):
84 | path = os.path.splitext(path)
85 | return ".".join((path[0], extension))
86 |
87 | def is_outdated(self, infile, outfile):
88 | if not os.path.exists(outfile):
89 | return True
90 |
91 | try:
92 | return os.path.getmtime(infile) > os.path.getmtime(outfile)
93 | except OSError:
94 | return True
95 |
96 |
97 | class SubProcessCompiler(CompilerBase):
98 | def execute_command(self, command, cwd=None, stdout_captured=None):
99 | """Execute a command at cwd, saving its normal output at
100 | stdout_captured. Errors, defined as nonzero return code or a failure
101 | to start execution, will raise a CompilerError exception with a
102 | description of the cause. They do not write output.
103 |
104 | This is file-system safe (any valid file names are allowed, even with
105 | spaces or crazy characters) and OS agnostic (existing and future OSes
106 | that Python supports should already work).
107 |
108 | The only thing weird here is that any incoming command arg item may
109 | itself be a tuple. This allows compiler implementations to look clean
110 | while supporting historical string config settings and maintaining
111 | backwards compatibility. Thus, we flatten one layer deep.
112 | ((env, foocomp), infile, (-arg,)) -> (env, foocomp, infile, -arg)
113 | """
114 | argument_list = []
115 | for flattening_arg in command:
116 | if isinstance(flattening_arg, (str,)):
117 | argument_list.append(flattening_arg)
118 | else:
119 | argument_list.extend(flattening_arg)
120 |
121 | # The first element in argument_list is the program that will be
122 | # executed; if it is '', then a PermissionError will be raised.
123 | # Thus empty arguments are filtered out from argument_list
124 | argument_list = list(filter(None, argument_list))
125 | stdout = None
126 | try:
127 | # We always catch stdout in a file, but we may not have a use for it.
128 | temp_file_container = (
129 | cwd or os.path.dirname(stdout_captured or "") or os.getcwd()
130 | )
131 | with NamedTemporaryFile(
132 | "wb", delete=False, dir=temp_file_container
133 | ) as stdout:
134 | compiling = subprocess.Popen(
135 | argument_list, cwd=cwd, stdout=stdout, stderr=subprocess.PIPE
136 | )
137 | _, stderr = compiling.communicate()
138 | set_std_streams_blocking()
139 |
140 | if compiling.returncode != 0:
141 | stdout_captured = None # Don't save erroneous result.
142 | raise CompilerError(
143 | f"{argument_list!r} exit code {compiling.returncode}\n{stderr}",
144 | command=argument_list,
145 | error_output=force_str(stderr),
146 | )
147 |
148 | # User wants to see everything that happened.
149 | if self.verbose:
150 | with open(stdout.name, "rb") as out:
151 | print(out.read())
152 | print(stderr)
153 | except OSError as e:
154 | stdout_captured = None # Don't save erroneous result.
155 | raise CompilerError(e, command=argument_list, error_output=str(e))
156 | finally:
157 | # Decide what to do with captured stdout.
158 | if stdout:
159 | if stdout_captured:
160 | shutil.move(
161 | stdout.name, os.path.join(cwd or os.curdir, stdout_captured)
162 | )
163 | else:
164 | os.remove(stdout.name)
165 |
--------------------------------------------------------------------------------
/tests/tests/test_forms.py:
--------------------------------------------------------------------------------
1 | from django import get_version as django_version
2 | from django.forms import Media
3 | from django.test import TestCase
4 |
5 | from pipeline.forms import PipelineFormMedia
6 |
7 | from ..utils import pipeline_settings
8 |
9 |
10 | @pipeline_settings(
11 | PIPELINE_COLLECTOR_ENABLED=False,
12 | STYLESHEETS={
13 | "styles1": {
14 | "source_filenames": (
15 | "pipeline/css/first.css",
16 | "pipeline/css/second.css",
17 | ),
18 | "output_filename": "styles1.min.css",
19 | },
20 | "styles2": {
21 | "source_filenames": ("pipeline/css/unicode.css",),
22 | "output_filename": "styles2.min.css",
23 | },
24 | "print": {
25 | "source_filenames": ("pipeline/css/urls.css",),
26 | "output_filename": "print.min.css",
27 | },
28 | },
29 | JAVASCRIPT={
30 | "scripts1": {
31 | "source_filenames": (
32 | "pipeline/js/first.js",
33 | "pipeline/js/second.js",
34 | ),
35 | "output_filename": "scripts1.min.js",
36 | },
37 | "scripts2": {
38 | "source_filenames": ("pipeline/js/application.js",),
39 | "output_filename": "scripts2.min.js",
40 | },
41 | },
42 | )
43 | class PipelineFormMediaTests(TestCase):
44 | """Unit tests for pipeline.forms.PipelineFormMedia."""
45 |
46 | @pipeline_settings(PIPELINE_ENABLED=True)
47 | def test_css_packages_with_pipeline_enabled(self):
48 | """Testing PipelineFormMedia.css_packages with PIPELINE_ENABLED=True"""
49 |
50 | class MyMedia(PipelineFormMedia):
51 | css_packages = {
52 | "all": ("styles1", "styles2"),
53 | "print": ("print",),
54 | }
55 |
56 | css = {"all": ("extra1.css", "extra2.css")}
57 |
58 | media = Media(MyMedia)
59 |
60 | self.assertEqual(
61 | MyMedia.css,
62 | {
63 | "all": [
64 | "extra1.css",
65 | "extra2.css",
66 | "/static/styles1.min.css",
67 | "/static/styles2.min.css",
68 | ],
69 | "print": ["/static/print.min.css"],
70 | },
71 | )
72 | self.assertEqual(MyMedia.css, media._css)
73 | expected_regex = [
74 | r''.format(path)
76 | for path in (
77 | "/static/extra1.css",
78 | "/static/extra2.css",
79 | "/static/styles1.min.css",
80 | "/static/styles2.min.css",
81 | )
82 | ] + [
83 | r''
85 | ]
86 | for rendered_node, expected_node in zip(media.render_css(), expected_regex):
87 | self.assertRegex(rendered_node, expected_node)
88 |
89 | @pipeline_settings(PIPELINE_ENABLED=False)
90 | def test_css_packages_with_pipeline_disabled(self):
91 | """Testing PipelineFormMedia.css_packages with PIPELINE_ENABLED=False"""
92 |
93 | class MyMedia(PipelineFormMedia):
94 | css_packages = {
95 | "all": ("styles1", "styles2"),
96 | "print": ("print",),
97 | }
98 |
99 | css = {"all": ("extra1.css", "extra2.css")}
100 |
101 | media = Media(MyMedia)
102 |
103 | self.assertEqual(
104 | MyMedia.css,
105 | {
106 | "all": [
107 | "extra1.css",
108 | "extra2.css",
109 | "pipeline/css/first.css",
110 | "pipeline/css/second.css",
111 | "pipeline/css/unicode.css",
112 | ],
113 | "print": ["pipeline/css/urls.css"],
114 | },
115 | )
116 | self.assertEqual(MyMedia.css, media._css)
117 |
118 | expected_regex = [
119 | ''.format(path)
121 | for path in (
122 | "/static/extra1.css",
123 | "/static/extra2.css",
124 | "/static/pipeline/css/first.css",
125 | "/static/pipeline/css/second.css",
126 | "/static/pipeline/css/unicode.css",
127 | )
128 | ] + [
129 | ''
131 | ]
132 | for rendered_node, expected_node in zip(media.render_css(), expected_regex):
133 | self.assertRegex(rendered_node, expected_node)
134 |
135 | @pipeline_settings(PIPELINE_ENABLED=True)
136 | def test_js_packages_with_pipeline_enabled(self):
137 | """Testing PipelineFormMedia.js_packages with PIPELINE_ENABLED=True"""
138 |
139 | class MyMedia(PipelineFormMedia):
140 | js_packages = ("scripts1", "scripts2")
141 | js = ("extra1.js", "extra2.js")
142 |
143 | media = Media(MyMedia)
144 |
145 | if django_version() < "3.1":
146 | script_tag = ''
147 | else:
148 | script_tag = ''
149 |
150 | self.assertEqual(
151 | MyMedia.js,
152 | [
153 | "extra1.js",
154 | "extra2.js",
155 | "/static/scripts1.min.js",
156 | "/static/scripts2.min.js",
157 | ],
158 | )
159 | self.assertEqual(MyMedia.js, media._js)
160 | self.assertEqual(
161 | media.render_js(),
162 | [
163 | script_tag % path
164 | for path in (
165 | "/static/extra1.js",
166 | "/static/extra2.js",
167 | "/static/scripts1.min.js",
168 | "/static/scripts2.min.js",
169 | )
170 | ],
171 | )
172 |
173 | @pipeline_settings(PIPELINE_ENABLED=False)
174 | def test_js_packages_with_pipeline_disabled(self):
175 | """Testing PipelineFormMedia.js_packages with PIPELINE_ENABLED=False"""
176 |
177 | class MyMedia(PipelineFormMedia):
178 | js_packages = ("scripts1", "scripts2")
179 | js = ("extra1.js", "extra2.js")
180 |
181 | media = Media(MyMedia)
182 |
183 | if django_version() < "3.1":
184 | script_tag = ''
185 | else:
186 | script_tag = ''
187 |
188 | self.assertEqual(
189 | MyMedia.js,
190 | [
191 | "extra1.js",
192 | "extra2.js",
193 | "pipeline/js/first.js",
194 | "pipeline/js/second.js",
195 | "pipeline/js/application.js",
196 | ],
197 | )
198 | self.assertEqual(MyMedia.js, media._js)
199 | self.assertEqual(
200 | media.render_js(),
201 | [
202 | script_tag % path
203 | for path in (
204 | "/static/extra1.js",
205 | "/static/extra2.js",
206 | "/static/pipeline/js/first.js",
207 | "/static/pipeline/js/second.js",
208 | "/static/pipeline/js/application.js",
209 | )
210 | ],
211 | )
212 |
--------------------------------------------------------------------------------
/docs/compilers.rst:
--------------------------------------------------------------------------------
1 | .. _ref-compilers:
2 |
3 | =========
4 | Compilers
5 | =========
6 |
7 | TypeScript compiler
8 | ======================
9 |
10 | The TypeScript compiler uses `TypeScript `_
11 | to compile your TypeScript code to JavaScript.
12 |
13 | To use it add this to your ``PIPELINE['COMPILERS']`` ::
14 |
15 | PIPELINE['COMPILERS'] = (
16 | 'pipeline.compilers.typescript.TypeScriptCompiler',
17 | )
18 |
19 | ``TYPE_SCRIPT_BINARY``
20 | ---------------------------------
21 |
22 | Command line to execute for TypeScript program.
23 | You will most likely change this to the location of ``tsc`` on your system.
24 |
25 | Defaults to ``'/usr/bin/env tsc'``.
26 |
27 | ``TYPE_SCRIPT_ARGUMENTS``
28 | ------------------------------------
29 |
30 | Additional arguments to use when ``tsc`` is called.
31 |
32 | Defaults to ``''``.
33 |
34 | Coffee Script compiler
35 | ======================
36 |
37 | The Coffee Script compiler uses `Coffee Script `_
38 | to compile your javascript.
39 |
40 | To use it add this to your ``PIPELINE['COMPILERS']`` ::
41 |
42 | PIPELINE['COMPILERS'] = (
43 | 'pipeline.compilers.coffee.CoffeeScriptCompiler',
44 | )
45 |
46 | ``COFFEE_SCRIPT_BINARY``
47 | ---------------------------------
48 |
49 | Command line to execute for coffee program.
50 | You will most likely change this to the location of coffee on your system.
51 |
52 | Defaults to ``'/usr/bin/env coffee'``.
53 |
54 | ``COFFEE_SCRIPT_ARGUMENTS``
55 | ------------------------------------
56 |
57 | Additional arguments to use when coffee is called.
58 |
59 | Defaults to ``''``.
60 |
61 | Live Script compiler
62 | ======================
63 |
64 | The LiveScript compiler uses `LiveScript `_
65 | to compile your javascript.
66 |
67 | To use it add this to your ``PIPELINE['COMPILERS']`` ::
68 |
69 | PIPELINE['COMPILERS'] = (
70 | 'pipeline.compilers.livescript.LiveScriptCompiler',
71 | )
72 |
73 | ``LIVE_SCRIPT_BINARY``
74 | ---------------------------------
75 |
76 | Command line to execute for LiveScript program.
77 | You will most likely change this to the location of lsc on your system.
78 |
79 | Defaults to ``'/usr/bin/env lsc'``.
80 |
81 | ``LIVE_SCRIPT_ARGUMENTS``
82 | ------------------------------------
83 |
84 | Additional arguments to use when lsc is called.
85 |
86 | Defaults to ``''``.
87 |
88 | LESS compiler
89 | =============
90 |
91 | The LESS compiler uses `LESS `_
92 | to compile your stylesheets.
93 |
94 | To use it add this to your ``PIPELINE['COMPILERS']`` ::
95 |
96 | PIPELINE['COMPILERS'] = (
97 | 'pipeline.compilers.less.LessCompiler',
98 | )
99 |
100 | ``LESS_BINARY``
101 | ------------------------
102 |
103 | Command line to execute for lessc program.
104 | You will most likely change this to the location of lessc on your system.
105 |
106 | Defaults to ``'/usr/bin/env lessc'``.
107 |
108 | ``LESS_ARGUMENTS``
109 | ---------------------------
110 |
111 | Additional arguments to use when lessc is called.
112 |
113 | Defaults to ``''``.
114 |
115 | SASS compiler
116 | =============
117 |
118 | The SASS compiler uses `SASS `_
119 | to compile your stylesheets.
120 |
121 | To use it add this to your ``PIPELINE['COMPILERS']`` ::
122 |
123 | PIPELINE['COMPILERS'] = (
124 | 'pipeline.compilers.sass.SASSCompiler',
125 | )
126 |
127 |
128 | ``SASS_BINARY``
129 | ------------------------
130 |
131 | Command line to execute for sass program.
132 | You will most likely change this to the location of sass on your system.
133 |
134 | Defaults to ``'/usr/bin/env sass'``.
135 |
136 | ``SASS_ARGUMENTS``
137 | ---------------------------
138 |
139 | Additional arguments to use when sass is called.
140 |
141 | Defaults to ``''``.
142 |
143 |
144 | Stylus compiler
145 | ===============
146 |
147 | The Stylus compiler uses `Stylus `_
148 | to compile your stylesheets.
149 |
150 | To use it add this to your ``PIPELINE['COMPILERS']`` ::
151 |
152 | PIPELINE['COMPILERS'] = (
153 | 'pipeline.compilers.stylus.StylusCompiler',
154 | )
155 |
156 |
157 | ``STYLUS_BINARY``
158 | --------------------------
159 |
160 | Command line to execute for stylus program.
161 | You will most likely change this to the location of stylus on your system.
162 |
163 | Defaults to ``'/usr/bin/env stylus'``.
164 |
165 | ``STYLUS_ARGUMENTS``
166 | -----------------------------
167 |
168 | Additional arguments to use when stylus is called.
169 |
170 | Defaults to ``''``.
171 |
172 |
173 | ES6 compiler
174 | ============
175 |
176 | The ES6 compiler uses `Babel `_
177 | to convert ES6+ code into vanilla ES5.
178 |
179 | Note that for files to be transpiled properly they must have the file extension **.es6**
180 |
181 | To use it add this to your ``PIPELINE['COMPILERS']`` ::
182 |
183 | PIPELINE['COMPILERS'] = (
184 | 'pipeline.compilers.es6.ES6Compiler',
185 | )
186 |
187 |
188 | ``BABEL_BINARY``
189 | --------------------------
190 |
191 | Command line to execute for babel program.
192 | You will most likely change this to the location of babel on your system.
193 |
194 | Defaults to ``'/usr/bin/env babel'``.
195 |
196 | ``BABEL_ARGUMENTS``
197 | -----------------------------
198 |
199 | Additional arguments to use when babel is called.
200 |
201 | Defaults to ``''``.
202 |
203 |
204 | Write your own compiler class
205 | =============================
206 |
207 | You can write your own compiler class, for example if you want to implement other types
208 | of compilers.
209 |
210 | To do so, you just have to create a class that inherits from ``pipeline.compilers.CompilerBase``
211 | and implements ``match_file`` and ``compile_file`` when needed.
212 |
213 | Finally, specify it in the tuple of compilers ``PIPELINE['COMPILERS']`` in the settings.
214 |
215 | Example
216 | -------
217 |
218 | A custom compiler for an imaginary compiler called jam ::
219 |
220 | from pipeline.compilers import CompilerBase
221 |
222 | class JamCompiler(CompilerBase):
223 | output_extension = 'js'
224 |
225 | def match_file(self, filename):
226 | return filename.endswith('.jam')
227 |
228 | def compile_file(self, infile, outfile, outdated=False, force=False):
229 | if not outdated and not force:
230 | return # No need to recompiled file
231 | return jam.compile(infile, outfile)
232 |
233 |
234 | 3rd Party Compilers
235 | ===================
236 |
237 | Here is an (in)complete list of 3rd party compilers that integrate with django-pipeline
238 |
239 | Compass (requires RubyGem)
240 | --------------------------
241 |
242 | :Creator:
243 | `Mila Labs `_
244 | :Description:
245 | Compass compiler for django-pipeline using the original Ruby gem.
246 | :Link:
247 | `https://github.com/mila-labs/django-pipeline-compass-rubygem`
248 |
249 | Compass (standalone)
250 | --------------------
251 |
252 | :Creator:
253 | `Vitaly Babiy `_
254 | :Description:
255 | django-pipeline-compass is a compiler for `django-pipeline `_. Making it really easy to use scss and compass with out requiring the compass gem.
256 | :Link:
257 | `https://github.com/vbabiy/django-pipeline-compass`
258 |
259 | Libsass (standalone)
260 | --------------------
261 |
262 | :Creator:
263 | `Johanderson Mogollon `_
264 | :Description:
265 | libsasscompiler is a compiler for `django-pipeline `_. Making it really easy to use scss/sass with the super fast libsass library.
266 | :Link:
267 | `https://github.com/sonic182/libsasscompiler`
268 |
--------------------------------------------------------------------------------
/tests/settings.py:
--------------------------------------------------------------------------------
1 | import glob
2 | import os
3 | import shutil
4 |
5 |
6 | def local_path(path):
7 | return os.path.join(os.path.dirname(__file__), path)
8 |
9 |
10 | DATABASES = {
11 | "default": {"ENGINE": "django.db.backends.sqlite3", "TEST_NAME": ":memory:"}
12 | }
13 |
14 | DEBUG = False
15 |
16 | SITE_ID = 1
17 |
18 | INSTALLED_APPS = [
19 | "django.contrib.contenttypes",
20 | "django.contrib.messages",
21 | "django.contrib.sites",
22 | "django.contrib.sessions",
23 | "django.contrib.staticfiles",
24 | "django.contrib.auth",
25 | "django.contrib.admin",
26 | "pipeline",
27 | "tests.tests",
28 | ]
29 |
30 |
31 | ROOT_URLCONF = "tests.urls"
32 |
33 | MIDDLEWARE = [
34 | "django.contrib.sessions.middleware.SessionMiddleware",
35 | "django.contrib.auth.middleware.AuthenticationMiddleware",
36 | "django.contrib.messages.middleware.MessageMiddleware",
37 | "django.middleware.common.CommonMiddleware",
38 | "django.middleware.csrf.CsrfViewMiddleware",
39 | ]
40 |
41 | MEDIA_URL = "/media/"
42 |
43 | MEDIA_ROOT = local_path("media")
44 |
45 | STATICFILES_STORAGE = "pipeline.storage.PipelineStorage"
46 | STATIC_ROOT = local_path("static/")
47 | STATIC_URL = "/static/"
48 | STATICFILES_DIRS = (("pipeline", local_path("assets/")),)
49 | STATICFILES_FINDERS = (
50 | "django.contrib.staticfiles.finders.FileSystemFinder",
51 | "django.contrib.staticfiles.finders.AppDirectoriesFinder",
52 | "pipeline.finders.PipelineFinder",
53 | )
54 |
55 | SECRET_KEY = "django-pipeline"
56 |
57 | PIPELINE = {
58 | "PIPELINE_ENABLED": True,
59 | "JS_COMPRESSOR": None,
60 | "CSS_COMPRESSOR": None,
61 | "STYLESHEETS": {
62 | "screen": {
63 | "source_filenames": (
64 | "pipeline/css/first.css",
65 | "pipeline/css/second.css",
66 | "pipeline/css/urls.css",
67 | ),
68 | "output_filename": "screen.css",
69 | },
70 | "screen_media": {
71 | "source_filenames": (
72 | "pipeline/css/first.css",
73 | "pipeline/css/second.css",
74 | "pipeline/css/urls.css",
75 | ),
76 | "output_filename": "screen_media.css",
77 | "extra_context": {
78 | "media": "screen and (min-width:500px)",
79 | },
80 | },
81 | "screen_title": {
82 | "source_filenames": (
83 | "pipeline/css/first.css",
84 | "pipeline/css/second.css",
85 | "pipeline/css/urls.css",
86 | ),
87 | "output_filename": "screen_title.css",
88 | "extra_context": {
89 | "title": "Default Style",
90 | },
91 | },
92 | },
93 | "JAVASCRIPT": {
94 | "scripts": {
95 | "source_filenames": (
96 | "pipeline/js/first.js",
97 | "pipeline/js/second.js",
98 | "pipeline/js/application.js",
99 | "pipeline/templates/**/*.jst",
100 | ),
101 | "output_filename": "scripts.js",
102 | },
103 | "scripts_async": {
104 | "source_filenames": (
105 | "pipeline/js/first.js",
106 | "pipeline/js/second.js",
107 | "pipeline/js/application.js",
108 | "pipeline/templates/**/*.jst",
109 | ),
110 | "output_filename": "scripts_async.js",
111 | "extra_context": {
112 | "async": True,
113 | },
114 | },
115 | "scripts_defer": {
116 | "source_filenames": (
117 | "pipeline/js/first.js",
118 | "pipeline/js/second.js",
119 | "pipeline/js/application.js",
120 | "pipeline/templates/**/*.jst",
121 | ),
122 | "output_filename": "scripts_defer.js",
123 | "extra_context": {
124 | "defer": True,
125 | },
126 | },
127 | "scripts_async_defer": {
128 | "source_filenames": (
129 | "pipeline/js/first.js",
130 | "pipeline/js/second.js",
131 | "pipeline/js/application.js",
132 | "pipeline/templates/**/*.jst",
133 | ),
134 | "output_filename": "scripts_async_defer.js",
135 | "extra_context": {
136 | "async": True,
137 | "defer": True,
138 | },
139 | },
140 | },
141 | }
142 |
143 | NODE_MODULES_PATH = local_path("../node_modules")
144 | NODE_BIN_PATH = os.path.join(NODE_MODULES_PATH, ".bin")
145 | NODE_EXE_PATH = shutil.which("node")
146 | JAVA_EXE_PATH = shutil.which("java")
147 | CSSTIDY_EXE_PATH = shutil.which("csstidy")
148 | HAS_NODE = bool(NODE_EXE_PATH)
149 | HAS_JAVA = bool(JAVA_EXE_PATH)
150 | HAS_CSSTIDY = bool(CSSTIDY_EXE_PATH)
151 |
152 | if HAS_NODE:
153 |
154 | def node_exe_path(command):
155 | exe_ext = ".cmd" if os.name == "nt" else ""
156 | return os.path.join(NODE_BIN_PATH, "{}{}".format(command, exe_ext))
157 |
158 | PIPELINE.update(
159 | {
160 | "SASS_BINARY": node_exe_path("sass"),
161 | "COFFEE_SCRIPT_BINARY": node_exe_path("coffee"),
162 | "COFFEE_SCRIPT_ARGUMENTS": ["--no-header"],
163 | "LESS_BINARY": node_exe_path("lessc"),
164 | "BABEL_BINARY": node_exe_path("babel"),
165 | "BABEL_ARGUMENTS": ["--presets", "es2015"],
166 | "STYLUS_BINARY": node_exe_path("stylus"),
167 | "LIVE_SCRIPT_BINARY": node_exe_path("lsc"),
168 | "LIVE_SCRIPT_ARGUMENTS": ["--no-header"],
169 | "YUGLIFY_BINARY": node_exe_path("yuglify"),
170 | "UGLIFYJS_BINARY": node_exe_path("uglifyjs"),
171 | "TERSER_BINARY": node_exe_path("terser"),
172 | "CSSMIN_BINARY": node_exe_path("cssmin"),
173 | "TYPE_SCRIPT_BINARY": node_exe_path("tsc"),
174 | }
175 | )
176 |
177 | if HAS_NODE and HAS_JAVA:
178 | PIPELINE.update(
179 | {
180 | "CLOSURE_BINARY": [
181 | JAVA_EXE_PATH,
182 | "-jar",
183 | os.path.join(
184 | NODE_MODULES_PATH,
185 | "google-closure-compiler-java",
186 | "compiler.jar",
187 | ),
188 | ],
189 | "YUI_BINARY": [
190 | JAVA_EXE_PATH,
191 | "-jar",
192 | glob.glob(
193 | os.path.join(NODE_MODULES_PATH, "yuicompressor", "build", "*.jar")
194 | )[0],
195 | ],
196 | }
197 | )
198 |
199 | if HAS_CSSTIDY:
200 | PIPELINE.update({"CSSTIDY_BINARY": CSSTIDY_EXE_PATH})
201 |
202 | TEMPLATES = [
203 | {
204 | "BACKEND": "django.template.backends.django.DjangoTemplates",
205 | "APP_DIRS": True,
206 | "DIRS": [local_path("templates")],
207 | "OPTIONS": {
208 | "context_processors": [
209 | "django.template.context_processors.request",
210 | "django.contrib.auth.context_processors.auth",
211 | "django.contrib.messages.context_processors.messages",
212 | ]
213 | },
214 | },
215 | {
216 | "BACKEND": "django.template.backends.jinja2.Jinja2",
217 | "APP_DIRS": True,
218 | "DIRS": [local_path("templates")],
219 | "OPTIONS": {"extensions": ["pipeline.jinja2.PipelineExtension"]},
220 | },
221 | ]
222 |
223 | LOGGING = {
224 | "version": 1,
225 | "disable_existing_loggers": False,
226 | "handlers": {
227 | "console": {
228 | "class": "logging.StreamHandler",
229 | },
230 | },
231 | "loggers": {
232 | "pipeline.templatetags.pipeline": {
233 | "handlers": ["console"],
234 | "level": "ERROR",
235 | },
236 | },
237 | }
238 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Pipeline documentation build configuration file, created by
2 | # sphinx-quickstart on Sat Apr 30 17:47:55 2011.
3 | #
4 | # This file is execfile()d with the current directory set to its containing dir.
5 | #
6 | # Note that not all possible configuration values are present in this
7 | # autogenerated file.
8 | #
9 | # All configuration values have a default; values that are commented out
10 | # serve to show the default.
11 |
12 | # If extensions (or modules to document with autodoc) are in another directory,
13 | # add these directories to sys.path here. If the directory is relative to the
14 | # documentation root, use os.path.abspath to make it absolute, like shown here.
15 | # sys.path.insert(0, os.path.abspath('.'))
16 | from datetime import datetime
17 |
18 | from pipeline import __version__ as pipeline_version
19 |
20 | # -- General configuration -----------------------------------------------------
21 |
22 | # If your documentation needs a minimal Sphinx version, state it here.
23 | # needs_sphinx = '1.0'
24 |
25 | # Add any Sphinx extension module names here, as strings. They can be extensions
26 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
27 | extensions = []
28 |
29 | # Add any paths that contain templates here, relative to this directory.
30 | templates_path = ["_templates"]
31 |
32 | # The suffix of source filenames.
33 | source_suffix = ".rst"
34 |
35 | # The encoding of source files.
36 | # source_encoding = 'utf-8-sig'
37 |
38 | # The master toctree document.
39 | master_doc = "index"
40 |
41 | # General information about the project.
42 | project = "django-pipeline"
43 | current_year = datetime.now().year
44 | copyright = "2011-{}, Timothée Peignier".format(current_year)
45 |
46 | # The version info for the project you're documenting, acts as replacement for
47 | # |version| and |release|, also used in various other places throughout the
48 | # built documents.
49 | #
50 | # The full version, including alpha/beta/rc tags.
51 | release = pipeline_version
52 | # The short X.Y version.
53 | version = ".".join(release.split(".")[:2])
54 |
55 | # The language for content autogenerated by Sphinx. Refer to documentation
56 | # for a list of supported languages.
57 | # language = None
58 |
59 | # There are two options for replacing |today|: either, you set today to some
60 | # non-false value, then it is used:
61 | # today = ''
62 | # Else, today_fmt is used as the format for a strftime call.
63 | # today_fmt = '%B %d, %Y'
64 |
65 | # List of patterns, relative to source directory, that match files and
66 | # directories to ignore when looking for source files.
67 | exclude_patterns = ["_build"]
68 |
69 | # The reST default role (used for this markup: `text`) to use for all documents.
70 | # default_role = None
71 |
72 | # If true, '()' will be appended to :func: etc. cross-reference text.
73 | # add_function_parentheses = True
74 |
75 | # If true, the current module name will be prepended to all description
76 | # unit titles (such as .. function::).
77 | # add_module_names = True
78 |
79 | # If true, sectionauthor and moduleauthor directives will be shown in the
80 | # output. They are ignored by default.
81 | # show_authors = False
82 |
83 | # The name of the Pygments (syntax highlighting) style to use.
84 | pygments_style = "sphinx"
85 |
86 | # A list of ignored prefixes for module index sorting.
87 | # modindex_common_prefix = []
88 |
89 |
90 | # -- Options for HTML output ---------------------------------------------------
91 |
92 | # The theme to use for HTML and HTML Help pages. See the documentation for
93 | # a list of builtin themes.
94 | html_theme = "default"
95 |
96 | # Theme options are theme-specific and customize the look and feel of a theme
97 | # further. For a list of options available for each theme, see the
98 | # documentation.
99 | # html_theme_options = {}
100 |
101 | # Add any paths that contain custom themes here, relative to this directory.
102 | # html_theme_path = []
103 |
104 | # The name for this set of Sphinx documents. If None, it defaults to
105 | # " v documentation".
106 | # html_title = None
107 |
108 | # A shorter title for the navigation bar. Default is the same as html_title.
109 | # html_short_title = None
110 |
111 | # The name of an image file (relative to this directory) to place at the top
112 | # of the sidebar.
113 | # html_logo = None
114 |
115 | # The name of an image file (within the static path) to use as favicon of the
116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
117 | # pixels large.
118 | # html_favicon = None
119 |
120 | # Add any paths that contain custom static files (such as style sheets) here,
121 | # relative to this directory. They are copied after the builtin static files,
122 | # so a file named "default.css" will overwrite the builtin "default.css".
123 | # html_static_path = ['_static']
124 |
125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
126 | # using the given strftime format.
127 | # html_last_updated_fmt = '%b %d, %Y'
128 |
129 | # If true, SmartyPants will be used to convert quotes and dashes to
130 | # typographically correct entities.
131 | # html_use_smartypants = True
132 |
133 | # Custom sidebar templates, maps document names to template names.
134 | # html_sidebars = {}
135 |
136 | # Additional templates that should be rendered to pages, maps page names to
137 | # template names.
138 | # html_additional_pages = {}
139 |
140 | # If false, no module index is generated.
141 | # html_domain_indices = True
142 |
143 | # If false, no index is generated.
144 | # html_use_index = True
145 |
146 | # If true, the index is split into individual pages for each letter.
147 | # html_split_index = False
148 |
149 | # If true, links to the reST sources are added to the pages.
150 | # html_show_sourcelink = True
151 |
152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
153 | # html_show_sphinx = True
154 |
155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
156 | # html_show_copyright = True
157 |
158 | # If true, an OpenSearch description file will be output, and all pages will
159 | # contain a tag referring to it. The value of this option must be the
160 | # base URL from which the finished HTML is served.
161 | # html_use_opensearch = ''
162 |
163 | # This is the file name suffix for HTML files (e.g. ".xhtml").
164 | # html_file_suffix = None
165 |
166 | # Output file base name for HTML help builder.
167 | htmlhelp_basename = "django-pipelinedoc"
168 |
169 |
170 | # -- Options for LaTeX output --------------------------------------------------
171 |
172 | # The paper size ('letter' or 'a4').
173 | # latex_paper_size = 'letter'
174 |
175 | # The font size ('10pt', '11pt' or '12pt').
176 | # latex_font_size = '10pt'
177 |
178 | # Grouping the document tree into LaTeX files. List of tuples
179 | # (source start file, target name, title, author, documentclass [howto/manual]).
180 | latex_documents = [
181 | (
182 | "index",
183 | "django-pipeline.tex",
184 | "Pipeline Documentation",
185 | "Timothée Peignier",
186 | "manual",
187 | ),
188 | ]
189 |
190 | # The name of an image file (relative to this directory) to place at the top of
191 | # the title page.
192 | # latex_logo = None
193 |
194 | # For "manual" documents, if this is true, then toplevel headings are parts,
195 | # not chapters.
196 | # latex_use_parts = False
197 |
198 | # If true, show page references after internal links.
199 | # latex_show_pagerefs = False
200 |
201 | # If true, show URL addresses after external links.
202 | # latex_show_urls = False
203 |
204 | # Additional stuff for the LaTeX preamble.
205 | # latex_preamble = ''
206 |
207 | # Documents to append as an appendix to all manuals.
208 | # latex_appendices = []
209 |
210 | # If false, no module index is generated.
211 | # latex_domain_indices = True
212 |
213 |
214 | # -- Options for manual page output --------------------------------------------
215 |
216 | # One entry per manual page. List of tuples
217 | # (source start file, name, description, authors, manual section).
218 | man_pages = [
219 | ("index", "django-pipeline", "Pipeline Documentation", ["Timothée Peignier"], 1)
220 | ]
221 |
--------------------------------------------------------------------------------
/pipeline/templatetags/pipeline.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import re
3 | import subprocess
4 |
5 | from django import template
6 | from django.contrib.staticfiles.storage import staticfiles_storage
7 | from django.template.base import VariableDoesNotExist
8 | from django.template.loader import render_to_string
9 | from django.utils.safestring import mark_safe
10 |
11 | from ..collector import default_collector
12 | from ..conf import settings
13 | from ..exceptions import CompilerError
14 | from ..packager import PackageNotFound, Packager
15 | from ..utils import guess_type
16 |
17 | logger = logging.getLogger(__name__)
18 |
19 | register = template.Library()
20 |
21 |
22 | class PipelineMixin:
23 | request = None
24 | _request_var = None
25 |
26 | @property
27 | def request_var(self):
28 | if not self._request_var:
29 | self._request_var = template.Variable("request")
30 | return self._request_var
31 |
32 | def package_for(self, package_name, package_type):
33 | package = {
34 | "js": getattr(settings, "JAVASCRIPT", {}).get(package_name, {}),
35 | "css": getattr(settings, "STYLESHEETS", {}).get(package_name, {}),
36 | }[package_type]
37 |
38 | if package:
39 | package = {package_name: package}
40 |
41 | packager = {
42 | "js": Packager(css_packages={}, js_packages=package),
43 | "css": Packager(css_packages=package, js_packages={}),
44 | }[package_type]
45 |
46 | return packager.package_for(package_type, package_name)
47 |
48 | def render(self, context):
49 | try:
50 | self.request = self.request_var.resolve(context)
51 | except VariableDoesNotExist:
52 | pass
53 |
54 | def render_compressed(self, package, package_name, package_type):
55 | """Render HTML for the package.
56 |
57 | If ``PIPELINE_ENABLED`` is ``True``, this will render the package's
58 | output file (using :py:meth:`render_compressed_output`). Otherwise,
59 | this will render the package's source files (using
60 | :py:meth:`render_compressed_sources`).
61 |
62 | Subclasses can override this method to provide custom behavior for
63 | determining what to render.
64 | """
65 | if settings.PIPELINE_ENABLED:
66 | return self.render_compressed_output(package, package_name, package_type)
67 | else:
68 | return self.render_compressed_sources(package, package_name, package_type)
69 |
70 | def render_compressed_output(self, package, package_name, package_type):
71 | """Render HTML for using the package's output file.
72 |
73 | Subclasses can override this method to provide custom behavior for
74 | rendering the output file.
75 | """
76 | method = getattr(self, f"render_{package_type}")
77 |
78 | return method(package, package.output_filename)
79 |
80 | def render_compressed_sources(self, package, package_name, package_type):
81 | """Render HTML for using the package's list of source files.
82 |
83 | Each source file will first be collected, if
84 | ``PIPELINE_COLLECTOR_ENABLED`` is ``True``.
85 |
86 | If there are any errors compiling any of the source files, an
87 | ``SHOW_ERRORS_INLINE`` is ``True``, those errors will be shown at
88 | the top of the page.
89 |
90 | Subclasses can override this method to provide custom behavior for
91 | rendering the source files.
92 | """
93 | if settings.PIPELINE_COLLECTOR_ENABLED:
94 | default_collector.collect(self.request)
95 |
96 | packager = Packager()
97 | method = getattr(self, f"render_individual_{package_type}")
98 |
99 | try:
100 | paths = packager.compile(package.paths)
101 | except CompilerError as e:
102 | if settings.SHOW_ERRORS_INLINE:
103 | method = getattr(self, f"render_error_{package_type}")
104 | return method(package_name, e)
105 | else:
106 | raise
107 |
108 | templates = packager.pack_templates(package)
109 |
110 | return method(package, paths, templates=templates)
111 |
112 | def render_error(self, package_type, package_name, e):
113 | # Remove any ANSI escape sequences in the output.
114 | error_output = re.sub(
115 | re.compile(r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]"),
116 | "",
117 | e.error_output,
118 | )
119 |
120 | return render_to_string(
121 | "pipeline/compile_error.html",
122 | {
123 | "package_type": package_type,
124 | "package_name": package_name,
125 | "command": subprocess.list2cmdline(e.command),
126 | "errors": error_output,
127 | },
128 | )
129 |
130 |
131 | class StylesheetNode(PipelineMixin, template.Node):
132 | def __init__(self, name):
133 | self.name = name
134 |
135 | def render(self, context):
136 | super().render(context)
137 | package_name = template.Variable(self.name).resolve(context)
138 |
139 | try:
140 | package = self.package_for(package_name, "css")
141 | except PackageNotFound:
142 | w = "Package %r is unknown. Check PIPELINE['STYLESHEETS'] in your settings."
143 | logger.warning(w, package_name)
144 | # fail silently, do not return anything if an invalid group is specified
145 | return ""
146 | return self.render_compressed(package, package_name, "css")
147 |
148 | def render_css(self, package, path):
149 | template_name = package.template_name or "pipeline/css.html"
150 | context = package.extra_context
151 | context.update(
152 | {
153 | "type": guess_type(path, "text/css"),
154 | "url": mark_safe(staticfiles_storage.url(path)),
155 | }
156 | )
157 | return render_to_string(template_name, context)
158 |
159 | def render_individual_css(self, package, paths, **kwargs):
160 | tags = [self.render_css(package, path) for path in paths]
161 | return "\n".join(tags)
162 |
163 | def render_error_css(self, package_name, e):
164 | return super().render_error("CSS", package_name, e)
165 |
166 |
167 | class JavascriptNode(PipelineMixin, template.Node):
168 | def __init__(self, name):
169 | self.name = name
170 |
171 | def render(self, context):
172 | super().render(context)
173 | package_name = template.Variable(self.name).resolve(context)
174 |
175 | try:
176 | package = self.package_for(package_name, "js")
177 | except PackageNotFound:
178 | w = "Package %r is unknown. Check PIPELINE['JAVASCRIPT'] in your settings."
179 | logger.warning(w, package_name)
180 | # fail silently, do not return anything if an invalid group is specified
181 | return ""
182 | return self.render_compressed(package, package_name, "js")
183 |
184 | def render_js(self, package, path):
185 | template_name = package.template_name or "pipeline/js.html"
186 | context = package.extra_context
187 | context.update(
188 | {
189 | "type": guess_type(path, "text/javascript"),
190 | "url": mark_safe(staticfiles_storage.url(path)),
191 | }
192 | )
193 | return render_to_string(template_name, context)
194 |
195 | def render_inline(self, package, js):
196 | context = package.extra_context
197 | context.update({"source": js})
198 | return render_to_string("pipeline/inline_js.html", context)
199 |
200 | def render_individual_js(self, package, paths, templates=None):
201 | tags = [self.render_js(package, js) for js in paths]
202 | if templates:
203 | tags.append(self.render_inline(package, templates))
204 | return "\n".join(tags)
205 |
206 | def render_error_js(self, package_name, e):
207 | return super().render_error("JavaScript", package_name, e)
208 |
209 |
210 | @register.tag
211 | def stylesheet(parser, token):
212 | try:
213 | tag_name, name = token.split_contents()
214 | except ValueError:
215 | e = (
216 | "%r requires exactly one argument: the name "
217 | "of a group in the PIPELINE.STYLESHEETS setting"
218 | )
219 | raise template.TemplateSyntaxError(e % token.split_contents()[0])
220 | return StylesheetNode(name)
221 |
222 |
223 | @register.tag
224 | def javascript(parser, token):
225 | try:
226 | tag_name, name = token.split_contents()
227 | except ValueError:
228 | e = (
229 | "%r requires exactly one argument: the name "
230 | "of a group in the PIPELINE.JAVASVRIPT setting"
231 | )
232 | raise template.TemplateSyntaxError(e % token.split_contents()[0])
233 | return JavascriptNode(name)
234 |
--------------------------------------------------------------------------------
/HISTORY.rst:
--------------------------------------------------------------------------------
1 | .. :changelog:
2 |
3 | History
4 | =======
5 |
6 | 4.1.0
7 | =====
8 | * Add support for Python 3.13
9 | * Add support for Django 5.2
10 |
11 | 4.0.0
12 | =====
13 | * Drop support for Python 3.8
14 | * Confirm support for Django 5.1 and drop support for Django 3.2
15 | * Use pyproject.toml
16 |
17 | 3.1.0
18 | =====
19 |
20 | * Replace deprecated .warn method with .warning
21 | * Update sourcemap paths when concatenating source files
22 | * Ensure correct compiler error styling and strip ANSI escape sequences
23 |
24 | 3.0.0
25 | =====
26 |
27 | * Use Pypy 3.10
28 | * Drop support for Python 3.7
29 | * Drop support for Django 2
30 | * Add Python 3.12 support
31 | * Add Django 4.2 support
32 | * Add Django 5.0 support
33 |
34 | 2.1.0
35 | =====
36 |
37 | * Update README.rst and add Pipeline overview image.
38 | * Add TypeScript compiler support.
39 | * Drop support for ``manifesto`` package.
40 | * Add support for Python 3.11 and Django 4.1
41 |
42 |
43 | 2.0.9
44 | =====
45 |
46 | * Fixed some typos in the docs.
47 | * Fixed string type of errors reported from compilers and compressors.
48 | * Updated github actions matrix for host and django support.
49 | * Updated github actions configuration to use modern versions of third-party
50 | actions.
51 | * Improved the packager to copy files to (S3) storage if it does not exist
52 | (#502).
53 |
54 |
55 | 2.0.8
56 | =====
57 |
58 | * Added **Django 4.0** compatibility. Thanks to @kevinmarsh (#760)
59 | * Add tests for **Django 4.0**, **Python 3.9** and **Python 3.10**.
60 | Thank to @kevinmarsh (#739)
61 | * Introduce CODE_OF_CONDUCT.md for the project. Thank to @hugovk (#758)
62 | * Add precision in the documentation for PipelineCachedStorage.
63 | Thank to @gatsinski (#739)
64 | * Drop support for slimit compressor (#765) due to package not released
65 | an official version for Python 3 and not any new package release from 2013.
66 | * Edit github actions matrix: django 3.2.9 support python 3.10, remove
67 | python 4.0 (doesn't exist) and exclude pypy-3.8 for django-main.
68 | * Add .pre-commit-config.yaml. Thanks to @hugovk (#762)
69 | * Update package.json due to CoffeeScript on NPM has moved to "coffeescript"
70 | * Update setup.py with Django 4.0 and Python 3.10
71 |
72 | 2.0.7
73 | =====
74 |
75 | * Added **Django 3.2** compatibility (Thanks to @jramnai in #751)
76 |
77 | 2.0.6
78 | ======
79 |
80 | * Added terser (JS compressor for ES5 and ES6) (Thanks to @felix-last in #696)
81 | * Moved tests to GitHub Actions: https://github.com/jazzband/django-pipeline/actions (#738)
82 | * Fixed deprecation warnings from Django (Thanks to @edelvalle in #731)
83 |
84 | 2.0.5
85 | ======
86 |
87 | * Adding **Django 3.1** compatibility.
88 | * CachedStaticFilesStorage is removed from Django. Add a check
89 | of the current version to prevent error while importing. Thank to @vmsp
90 | * Context in django.template.base is removed from Django and
91 | not used anymore in django-pipeline.
92 | * Fixing widgets tests of django-pipeline due to Media.render_js change in
93 | Django. More information in Django ticket #31892
94 |
95 | 2.0.4
96 | ======
97 |
98 | * Adding **css-html-js-minify** support to compress JS and CSS.
99 | * Update compressors documentation with css-html-js-minify.
100 | * Create tests for css-html-js-minify compressor.
101 | * Optimization by grouping the tests yuglify compressor.
102 |
103 | 2.0.3
104 | ======
105 |
106 | * Remove futures from pipeline **setup.py** requirements.
107 |
108 | 2.0.2
109 | =====
110 |
111 | * Fix Middleware to properly decode HTML. Thank to @gatsinski
112 | * Keep mimetypes as str. Thank to @benspaulding
113 | * Based on #642 add 'NonPackagingPipelineManifestStorage' and update
114 | the documentation: **storages.rst**. Thank to @kronion
115 |
116 | 2.0.1
117 | =====
118 |
119 | * Add subclass of ManifestStaticFilesStorage. Thank to @jhpinson
120 | * Change the documentation to use PipelineManifestStorage in configuration
121 | instead of PipelineCachedStorage now deprecated.
122 | * Change import MutableMapping from collections.abc. Thank to @colons
123 |
124 | 2.0.0
125 | =====
126 |
127 | * **Definitely drop the support of Python 2**.
128 | * Drop support for Python 3.5 (not compatible with PEP 498).
129 | * Remove 'decorator.py' how was used for backward compatibility
130 | between python 2 and 3 for metaclass inheritance on PipelineFormMedia.
131 | * Replace 'format' by 'fstring' (PEP 498: Literal String Interpolation).
132 | * Remove of old imports form 'django.utils.six' and these fixes (1.7.0).
133 | * Remove tests of uncovered versions of Python and Django.
134 | * Replace tests for Pypy by Pypy3.
135 | * Explicitly specify when files are read / write in binary mode.
136 | * Set opening files for tests to deal with universal newlines.
137 | * Upgrade documentation version to 2.0 to follow the project version.
138 |
139 | 1.7.0
140 | =====
141 |
142 | * Release the last major version of django-pipeline working on Python 2.
143 | * Thank you for all the modifications made since version 1.6.14, which we cannot quote.
144 | * Apply an optimization to save time during development. Thank to @blankser
145 | * Edit setup.py to follow the recommendation of the documentation. Thank to @shaneikennedy
146 | * Add tests for Django 3.0 and Python 3.8
147 | * Add alternatives imports for django.utils.six, who has been removed in Django 3.0
148 |
149 | 1.6.14
150 | ======
151 |
152 | * Fix packaging issues.
153 |
154 | 1.6.13
155 | ======
156 |
157 | * Fix forward-slashed paths on Windows. Thanks to @etiago
158 | * Fix CSS URL detector to match quotes correctly. Thanks to @vskh
159 | * Add a compiler_options dict to compile, to allow passing options to custom
160 | compilers. Thanks to @sassanh
161 | * Verify support for Django 1.11. Thanks to @jwhitlock
162 |
163 | 1.6.12
164 | ======
165 |
166 | * Supports Django 1.11
167 | * Fix a bug with os.rename on windows. Thanks to @wismill
168 | * Fix to view compile error if happens. Thanks to @brawaga
169 | * Add support for Pipeline CSS/JS packages in forms and widgets. Thanks to @chipx86
170 |
171 | 1.6.11
172 | ======
173 |
174 | * Fix performance regression. Thanks to Christian Hammond.
175 |
176 | 1.6.10
177 | ======
178 |
179 | * Added Django 1.10 compatiblity issues. Thanks to Austin Pua and Silvan Spross.
180 | * Documentation improvements. Thanks to Chris Streeter.
181 |
182 | 1.6.9
183 | =====
184 |
185 | * Various build improvements.
186 | * Improved setup.py classifiers. Thanks to Sobolev Nikita.
187 | * Documentation improvements. Thanks to Adam Chainz.
188 |
189 | 1.6.8
190 | =====
191 |
192 | * Made templatetags easier to subclass for special rendering behavior. Thanks
193 | to Christian Hammond.
194 | * Updated the link to readthedocs. Thanks to Corey Farwell.
195 | * Fixed some log messages to correctly refer to the new PIPELINE settings
196 | tructure. Thanks to Alvin Mites.
197 | * Changed file outdated checks to use os.path methods directly, avoiding
198 | potential SuspiciousFileOperation errors which could appear with some django
199 | storage configurations.
200 |
201 | 1.6.7
202 | =====
203 |
204 | * Add a view for collecting static files before serving them. This behaves like
205 | django's built-in ``static`` view and allows running the collector for
206 | images, fonts, and other static files that do not need to be compiled. Thanks
207 | to Christian Hammond.
208 | * Update documentation for the ES6Compiler to clarify filename requirements.
209 | Thanks to Nathan Cox.
210 | * Add error output for compiler errors within the browser. This provides for a
211 | much better experience when compiling files from the devserver. Thanks to
212 | Christian Hammond.
213 | * Make unit tests run against Django 1.6 and 1.7. Thanks to Sławek Ehlert.
214 |
215 | 1.6.6
216 | =====
217 |
218 | * Fix filtering-out of files which require a finder to locate.
219 | * Allow compilers to override the output path.
220 | * Fix error reporting when a compiler fails to execute.
221 | * Fix IOErrors when running collectstatic with some nodejs-based compilers and
222 | compressors. Thanks to Frankie Dintino.
223 | * Fix compatibility of unit tests when running on Windows. Thanks to Frankie
224 | Dintino.
225 | * Add unit tests for compilers and compressors. Thanks to Frankie Dintino.
226 |
227 | 1.6.5
228 | =====
229 |
230 | * Fix Django < 1.8 compatibility. Thanks to David Trowbridge.
231 | * Allow to disable collector during development. Thanks to Leonardo Orozco.
232 |
233 | 1.6.4
234 | =====
235 |
236 | * Fix compressor subprocess calls.
237 |
238 | 1.6.3
239 | =====
240 |
241 | * Fix compressor command flattening.
242 |
243 | 1.6.2
244 | =====
245 |
246 | * Remove subprocess32 usage since it breaks universal support.
247 |
248 | 1.6.1
249 | =====
250 |
251 | * Fix path quoting issues. Thanks to Chad Miller.
252 | * Use subprocess32 package when possible.
253 | * Documentation fixes. Thanks to Sławek Ehlert and Jannis Leidel.
254 |
255 | 1.6.0
256 | =====
257 |
258 | * Add full support for Django 1.9.
259 | * Drop support for Django 1.7.
260 | * Drop support for Python 2.6.
261 | * **BACKWARD INCOMPATIBLE** : Change configuration settings.
262 |
--------------------------------------------------------------------------------
/docs/configuration.rst:
--------------------------------------------------------------------------------
1 | .. _ref-configuration:
2 |
3 | =============
4 | Configuration
5 | =============
6 |
7 |
8 | Configuration and list of available settings for Pipeline. Pipeline settings are namespaced in a PIPELINE dictionary in your project settings, e.g.: ::
9 |
10 | PIPELINE = {
11 | 'PIPELINE_ENABLED': True,
12 | 'JAVASCRIPT': {
13 | 'stats': {
14 | 'source_filenames': (
15 | 'js/jquery.js',
16 | 'js/d3.js',
17 | 'js/collections/*.js',
18 | 'js/application.js',
19 | ),
20 | 'output_filename': 'js/stats.js',
21 | }
22 | }
23 | }
24 |
25 |
26 | Specifying files
27 | ================
28 |
29 | You specify groups of files to be compressed in your settings. You can use glob
30 | syntax to select multiples files.
31 |
32 | The basic syntax for specifying CSS/JavaScript groups files is ::
33 |
34 | PIPELINE = {
35 | 'STYLESHEETS': {
36 | 'colors': {
37 | 'source_filenames': (
38 | 'css/core.css',
39 | 'css/colors/*.css',
40 | 'css/layers.css'
41 | ),
42 | 'output_filename': 'css/colors.css',
43 | 'extra_context': {
44 | 'media': 'screen,projection',
45 | },
46 | },
47 | },
48 | 'JAVASCRIPT': {
49 | 'stats': {
50 | 'source_filenames': (
51 | 'js/jquery.js',
52 | 'js/d3.js',
53 | 'js/collections/*.js',
54 | 'js/application.js',
55 | ),
56 | 'output_filename': 'js/stats.js',
57 | }
58 | }
59 | }
60 |
61 | Group options
62 | -------------
63 |
64 | ``source_filenames``
65 | ....................
66 |
67 | **Required**
68 |
69 | Is a tuple with the source files to be compressed.
70 | The files are concatenated in the order specified in the tuple.
71 |
72 |
73 | ``output_filename``
74 | ...................
75 |
76 | **Required**
77 |
78 | Is the filename of the (to be) compressed file.
79 |
80 | ``variant``
81 | ...........
82 |
83 | **Optional**
84 |
85 | Is the variant you want to apply to your CSS. This allow you to embed images
86 | and fonts in CSS with data-URI.
87 | Allowed values are : ``None`` and ``datauri``.
88 |
89 | Defaults to ``None``.
90 |
91 | ``template_name``
92 | .................
93 |
94 | **Optional**
95 |
96 | Name of the template used to render ``