├── .github
├── FUNDING.yml
└── workflows
│ └── build.yml
├── .gitignore
├── .gitmodules
├── .idea
├── flask-assets.iml
└── modules.xml
├── .travis.yml
├── CHANGES
├── LICENSE
├── MANIFEST.in
├── README.rst
├── RELEASING
├── TODO
├── docs
├── .gitignore
├── Makefile
├── conf.py
├── index.rst
└── make.bat
├── example
├── .gitignore
├── app.py
├── static
│ ├── style1.css
│ └── style2.css
└── templates
│ └── index.html
├── fabfile.py
├── requirements.in
├── requirements.txt
├── setup.py
├── src
└── flask_assets.py
├── tests
├── __init__.py
├── bp_for_test
│ ├── __init__.py
│ └── static
│ │ └── README
├── conftest.py
├── helpers.py
├── test_config.py
├── test_env.py
└── test_integration.py
└── tox.ini
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | patreon: elsdoerfer
2 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | tests:
13 | name: tests
14 | strategy:
15 | matrix:
16 | os: [ubuntu-latest, macos-latest, windows-latest]
17 | python: ['3.8', '3.9', '3.10', '3.11', '3.12']
18 | fail-fast: false
19 | runs-on: ${{ matrix.os }}
20 | steps:
21 | - uses: actions/checkout@v2
22 | - uses: actions/setup-python@v2
23 | with:
24 | python-version: ${{ matrix.python }}
25 | - run: python -m pip install --upgrade pip wheel
26 | - run: pip install tox tox-gh-actions
27 | - run: tox
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.swp
3 | *.un~
4 |
5 | /.tox
6 | /LOCAL_TODO*
7 |
8 | # IDEs
9 | /flask-assets.wpr
10 | /.idea/*
11 |
12 | # distutils stuff
13 | /build/
14 | /dist/
15 | /src/Flask_Assets.egg-info/
16 | venv
17 | tests/static
18 | .eggs/
19 |
20 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "sphinx-themes"]
2 | path = docs/_themes
3 | url = https://github.com/pallets/flask-sphinx-themes.git
4 |
--------------------------------------------------------------------------------
/.idea/flask-assets.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - 2.6
4 | - 2.7
5 | - 3.3
6 | - 3.5
7 | - pypy
8 | env:
9 | - FLASK=0.8
10 | - FLASK=0.9
11 | - FLASK=0.10
12 | - FLASK=0.11
13 | install:
14 | - pip install Flask==$FLASK
15 | - pip install -r requirements-dev.pip --allow-external webassets
16 | script:
17 | - nosetests tests
18 | notifications:
19 | email:
20 | - michael@elsdoerfer.com
21 | branches:
22 | only:
23 | - master
24 | matrix:
25 | exclude:
26 | # These do not support Python 3 yet.
27 | - python: 3.3
28 | env: FLASK=0.8
29 | - python: 3.3
30 | env: FLASK=0.9
31 | - python: 3.5
32 | env: FLASK=0.8
33 | - python: 3.5
34 | env: FLASK=0.9
35 |
--------------------------------------------------------------------------------
/CHANGES:
--------------------------------------------------------------------------------
1 | 2.1.1 (Unreleased)
2 | - Drop Flask-Script legacy support
3 |
4 | 2.1.0 (2023-10-22)
5 | - Drop Python 2.x support.
6 | - Migrate from nose to pytest.
7 | - Migrate from Travis to GitHub Actions.
8 | - Test against Python 3.8 - 3.12.
9 | - Fix compatibility with Flask 2.0 and 3.0.
10 |
11 | 2.0 (2019-12-20)
12 | - Compatibility with webassets 2.0.
13 |
14 | 0.12 (2016-08-18)
15 | - Added registration of Flask CLI commands using `flask.commands`
16 | entrypoint group. (Jiri Kuncar)
17 | - Added an optional support for FlaskAzureStorage when
18 | `FLASK_ASSETS_USE_AZURE` is set. (Alejo Arias)
19 | - Updated Flask extension imports for compatibility with Flask 0.11.
20 | (Andy Driver) (fixes #102)
21 | - Fixed generation of absolute urls using //. (fixes #73)
22 | - Fixed Flask-Script assets build command. (Frank Tackitt)
23 |
24 | 0.11 (2015-08-21)
25 | - Match webassets 0.11.
26 | - Option to use Flask-CDN (James Elkins).
27 |
28 | 0.10 (2014-07-03)
29 | This release is compatible with webassets 0.10.
30 |
31 | 0.9 (2014-02-20)
32 | This release is compatible with webassets 0.9.
33 | flask-assets now support Python 3, and drops support for Python 2.5.
34 |
35 | - Support for Flask-S3 (Erik Taubeneck).
36 | - Support latest Flask-Script (Chris Hacken).
37 |
38 | 0.8 (2012-11-23)
39 | This release is compatible with webassets 0.8.
40 |
41 | - Flask-Script's ``build`` command now has ``--parse-templates`` option.
42 | - ``Environment`` class now has ``from_yaml`` and ``from_module``
43 | shortcuts (Sean Lynch).
44 | - Jinja2 filter uses the Flask template environment.
45 | - Fixed PySscss filter.
46 |
47 | 0.7 (2012-04-11)
48 | This release is compatible with webassets 0.7.
49 |
50 | - Now officially requires at least Flask 0.8, so it can use the new
51 | extension import system, but using the compatibility module, older
52 | Flask versions should work fine as well:
53 | http://flask.pocoo.org/docs/extensions/
54 | - Support Python 2.5.
55 | - Allow customizing the backend of ``ManageAssets`` command.
56 | - Due to webassets 0.7, the cssrewrite filter now works with Blueprints.
57 |
58 | 0.6.2 (2011-10-12)
59 | - Fixed Blueprint/Module resolving in output path.
60 |
61 | 0.6.1 (2011-10-10)
62 | - Building in 0.6 was very much broken (thanks Oliver Tonnhofer).
63 | - A custom "static_folder" for a Flask app or Blueprint/Module is now
64 | supported.
65 |
66 | 0.6 (2011-10-03)
67 | - Support webassets 0.6.
68 | - Fixed use of wrong Flask app in some cases.
69 | - Fixed init_app() usage (Oliver Tonnhofer)
70 | - Python 2.5 compatibility (Ron DuPlain)
71 |
72 | 0.5.1 (2011-08-12)
73 | - New version numbering scheme. The major and minor
74 | version numbers will now follow along with the version
75 | of webassets the Flask-Assets release was written
76 | against, and is guaranteed to be compatible with.
77 | - Support for Blueprints (Flask 0.7).
78 | - Fixed usage for incorrect request context during URL
79 | generation (thank you, julen).
80 |
81 | 0.2.2 (2011-05-27)
82 | - Really fix the ManageAssets command.
83 |
84 | 0.2.1 (2011-04-28)
85 | - Fixed the ManageAssets command to work with the current
86 | Flask-Script version.
87 |
88 | 0.2 (2011-03-30)
89 | - Support for init_app() protocol, multiple applications.
90 | - Integrate with Flask-Script, provide management command.
91 | - Properly support Flask modules, with the ability to reference
92 | the module's static files in bundles (Olivier Poitrey).
93 |
94 | 0.1 (2010-09-24)
95 | Initial release.
96 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010, Michael Elsdörfer
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions
6 | are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above
12 | copyright notice, this list of conditions and the following
13 | disclaimer in the documentation and/or other materials
14 | provided with the distribution.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
19 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
20 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
21 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
22 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 | POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE CHANGES README.rst *.py
2 |
3 | recursive-include docs *
4 | prune docs/_build
5 | prune docs/_themes/.git
6 |
7 | recursive-include tests *
8 | recursive-exclude tests *.pyc
9 |
10 | recursive-include example *
11 | recursive-exclude example *.pyc
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Integrates the `webassets`_ library with Flask, adding support for
2 | merging, minifying and compiling CSS and Javascript files.
3 |
4 | Documentation:
5 | https://flask-assets.readthedocs.io/
6 |
7 | .. _webassets: http://github.com/miracle2k/webassets
8 |
--------------------------------------------------------------------------------
/RELEASING:
--------------------------------------------------------------------------------
1 | - Update CHANGES file
2 | - Update version number in __init__.py
3 | - Update version number in latest "Upgrading" section in docs
4 | - git tag -a ...
5 | - ./setup.py sdist bdist_wheel upload
6 | - scp sdist file to my own server
7 | - Don't forget to push --all and push --tags.
8 |
9 |
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | Maybe add support for automatic bundle-loading from assets.py files,
2 | Django-like. However, since we have no single, global environment, we'd
3 | prefer to collect all "Bundle" instances from a module, rather than
4 | attempting something like g.assets_env.register().
5 |
6 | Using loaders is currently somewhat verbose, it'd be more micro-framework-like
7 | if we could say "assets_env.load('yaml', ...)".
8 |
9 | Now that we officially require Flask 0.8, and are no longer testing
10 | older versions, remove the support code for those older versions.
11 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | /_build/
--------------------------------------------------------------------------------
/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 pickle json htmlhelp qthelp latex 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 " pickle to make pickle files"
22 | @echo " json to make JSON files"
23 | @echo " htmlhelp to make HTML files and a HTML help project"
24 | @echo " qthelp to make HTML files and a qthelp project"
25 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
26 | @echo " changes to make an overview of all changed/added/deprecated items"
27 | @echo " linkcheck to check all external links for integrity"
28 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
29 |
30 | clean:
31 | -rm -rf $(BUILDDIR)/*
32 |
33 | html:
34 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
35 | @echo
36 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
37 |
38 | dirhtml:
39 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
40 | @echo
41 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
42 |
43 | pickle:
44 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
45 | @echo
46 | @echo "Build finished; now you can process the pickle files."
47 |
48 | json:
49 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
50 | @echo
51 | @echo "Build finished; now you can process the JSON files."
52 |
53 | htmlhelp:
54 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
55 | @echo
56 | @echo "Build finished; now you can run HTML Help Workshop with the" \
57 | ".hhp project file in $(BUILDDIR)/htmlhelp."
58 |
59 | qthelp:
60 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
61 | @echo
62 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
63 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
64 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-Assets.qhcp"
65 | @echo "To view the help file:"
66 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Assets.qhc"
67 |
68 | latex:
69 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
70 | @echo
71 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
72 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
73 | "run these through (pdf)latex."
74 |
75 | changes:
76 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
77 | @echo
78 | @echo "The overview file is in $(BUILDDIR)/changes."
79 |
80 | linkcheck:
81 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
82 | @echo
83 | @echo "Link check complete; look for any errors in the above output " \
84 | "or in $(BUILDDIR)/linkcheck/output.txt."
85 |
86 | doctest:
87 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
88 | @echo "Testing of doctests in the sources finished, look at the " \
89 | "results in $(BUILDDIR)/doctest/output.txt."
90 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Flask-Assets documentation build configuration file, created by
4 | # sphinx-quickstart on Fri Aug 6 14:01:08 2010.
5 | #
6 | # This file is execfile()d with the current directory set to its containing dir.
7 | #
8 | # Note that not all possible configuration values are present in this
9 | # autogenerated file.
10 | #
11 | # All configuration values have a default; values that are commented out
12 | # serve to show the default.
13 |
14 | import sys, os
15 |
16 | # If extensions (or modules to document with autodoc) are in another directory,
17 | # add these directories to sys.path here. If the directory is relative to the
18 | # documentation root, use os.path.abspath to make it absolute, like shown here.
19 | #sys.path.append(os.path.abspath('.'))
20 |
21 | sys.path.append(os.path.abspath('_themes'))
22 |
23 |
24 | # make sure we are documenting the local version with autodoc
25 | sys.path.insert(0, os.path.abspath('../src'))
26 | import flask_assets as flaskassets
27 |
28 |
29 | # -- General configuration -----------------------------------------------------
30 |
31 | # Add any Sphinx extension module names here, as strings. They can be extensions
32 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
33 | extensions = [
34 | 'sphinx.ext.autodoc',
35 | 'sphinx.ext.coverage',
36 | 'sphinx.ext.doctest',
37 | 'sphinx.ext.intersphinx',
38 | 'sphinx.ext.viewcode',
39 | ]
40 |
41 | # Add any paths that contain templates here, relative to this directory.
42 | templates_path = ['_templates']
43 |
44 | # The suffix of source filenames.
45 | source_suffix = '.rst'
46 |
47 | # The encoding of source files.
48 | #source_encoding = 'utf-8'
49 |
50 | # The master toctree document.
51 | master_doc = 'index'
52 |
53 | # General information about the project.
54 | project = u'Flask-Assets'
55 | copyright = u'2010, Michael Elsdörfer'
56 |
57 | # The version info for the project you're documenting, acts as replacement for
58 | # |version| and |release|, also used in various other places throughout the
59 | # built documents.
60 | #
61 | # The short X.Y version.
62 | version = ".".join(map(str, flaskassets.__version__))
63 | # The full version, including alpha/beta/rc tags.
64 | release = version
65 |
66 | # The language for content autogenerated by Sphinx. Refer to documentation
67 | # for a list of supported languages.
68 | #language = None
69 |
70 | # There are two options for replacing |today|: either, you set today to some
71 | # non-false value, then it is used:
72 | #today = ''
73 | # Else, today_fmt is used as the format for a strftime call.
74 | #today_fmt = '%B %d, %Y'
75 |
76 | # List of documents that shouldn't be included in the build.
77 | #unused_docs = []
78 |
79 | # List of directories, relative to source directory, that shouldn't be searched
80 | # for source files.
81 | exclude_trees = ['_build']
82 |
83 | # The reST default role (used for this markup: `text`) to use for all documents.
84 | #default_role = None
85 |
86 | # If true, '()' will be appended to :func: etc. cross-reference text.
87 | #add_function_parentheses = True
88 |
89 | # If true, the current module name will be prepended to all description
90 | # unit titles (such as .. function::).
91 | #add_module_names = True
92 |
93 | # If true, sectionauthor and moduleauthor directives will be shown in the
94 | # output. They are ignored by default.
95 | #show_authors = False
96 |
97 | # The name of the Pygments (syntax highlighting) style to use.
98 | pygments_style = 'sphinx'
99 |
100 | # A list of ignored prefixes for module index sorting.
101 | #modindex_common_prefix = []
102 |
103 |
104 | # -- Options for HTML output ---------------------------------------------------
105 |
106 | # The theme to use for HTML and HTML Help pages. Major themes that come with
107 | # Sphinx are currently 'default' and 'sphinxdoc'.
108 | html_theme = 'flask_small'
109 |
110 | # Theme options are theme-specific and customize the look and feel of a theme
111 | # further. For a list of options available for each theme, see the
112 | # documentation.
113 | html_theme_options = {'github_fork': 'miracle2k/flask-assets', 'index_logo': False}
114 |
115 | # Add any paths that contain custom themes here, relative to this directory.
116 | html_theme_path = ['_themes']
117 |
118 | # The name for this set of Sphinx documents. If None, it defaults to
119 | # " v documentation".
120 | #html_title = None
121 |
122 | # A shorter title for the navigation bar. Default is the same as html_title.
123 | #html_short_title = None
124 |
125 | # The name of an image file (relative to this directory) to place at the top
126 | # of the sidebar.
127 | #html_logo = None
128 |
129 | # The name of an image file (within the static path) to use as favicon of the
130 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
131 | # pixels large.
132 | #html_favicon = None
133 |
134 | # Add any paths that contain custom static files (such as style sheets) here,
135 | # relative to this directory. They are copied after the builtin static files,
136 | # so a file named "default.css" will overwrite the builtin "default.css".
137 | html_static_path = ['_static']
138 |
139 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
140 | # using the given strftime format.
141 | #html_last_updated_fmt = '%b %d, %Y'
142 |
143 | # If true, SmartyPants will be used to convert quotes and dashes to
144 | # typographically correct entities.
145 | #html_use_smartypants = True
146 |
147 | # Custom sidebar templates, maps document names to template names.
148 | #html_sidebars = {}
149 |
150 | # Additional templates that should be rendered to pages, maps page names to
151 | # template names.
152 | #html_additional_pages = {}
153 |
154 | # If false, no module index is generated.
155 | #html_use_modindex = True
156 |
157 | # If false, no index is generated.
158 | #html_use_index = True
159 |
160 | # If true, the index is split into individual pages for each letter.
161 | #html_split_index = False
162 |
163 | # If true, links to the reST sources are added to the pages.
164 | #html_show_sourcelink = True
165 |
166 | # If true, an OpenSearch description file will be output, and all pages will
167 | # contain a tag referring to it. The value of this option must be the
168 | # base URL from which the finished HTML is served.
169 | #html_use_opensearch = ''
170 |
171 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
172 | #html_file_suffix = ''
173 |
174 | # Output file base name for HTML help builder.
175 | htmlhelp_basename = 'Flask-Assetsdoc'
176 |
177 |
178 | # -- Options for LaTeX output --------------------------------------------------
179 |
180 | # The paper size ('letter' or 'a4').
181 | #latex_paper_size = 'letter'
182 |
183 | # The font size ('10pt', '11pt' or '12pt').
184 | #latex_font_size = '10pt'
185 |
186 | # Grouping the document tree into LaTeX files. List of tuples
187 | # (source start file, target name, title, author, documentclass [howto/manual]).
188 | latex_documents = [
189 | ('index', 'Flask-Assets.tex', u'Flask-Assets Documentation',
190 | u'Michael Elsdörfer', 'manual'),
191 | ]
192 |
193 | # The name of an image file (relative to this directory) to place at the top of
194 | # the title page.
195 | #latex_logo = None
196 |
197 | # For "manual" documents, if this is true, then toplevel headings are parts,
198 | # not chapters.
199 | #latex_use_parts = False
200 |
201 | # Additional stuff for the LaTeX preamble.
202 | #latex_preamble = ''
203 |
204 | # Documents to append as an appendix to all manuals.
205 | #latex_appendices = []
206 |
207 | # If false, no module index is generated.
208 | #latex_use_modindex = True
209 |
210 |
211 | # Example configuration for intersphinx: refer to the Python standard library.
212 | intersphinx_mapping = {
213 | 'python': ('https://docs.python.org/', None),
214 | 'webassets': ('https://webassets.readthedocs.io/en/latest/', None),
215 | }
216 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Flask-Assets
2 | ============
3 |
4 | .. currentmodule:: flask_assets
5 |
6 | Flask-Assets helps you to integrate `webassets`_ into your `Flask`_
7 | application.
8 |
9 | .. _webassets: http://github.com/miracle2k/webassets
10 | .. _Flask: http://flask.pocoo.org/
11 |
12 |
13 | Installation
14 | ------------
15 |
16 | Install the extension with one of the following commands::
17 |
18 | $ easy_install Flask-Assets
19 |
20 | or alternatively if you have pip installed::
21 |
22 | $ pip install Flask-Assets
23 |
24 |
25 | Usage
26 | -----
27 |
28 | You initialize the app by creating an :class:`Environment` instance, and
29 | registering your assets with it in the form of so called *bundles*.
30 |
31 | .. code-block:: python
32 |
33 | from flask import Flask
34 | from flask_assets import Environment, Bundle
35 |
36 | app = Flask(__name__)
37 | assets = Environment(app)
38 |
39 | js = Bundle('jquery.js', 'base.js', 'widgets.js',
40 | filters='jsmin', output='gen/packed.js')
41 | assets.register('js_all', js)
42 |
43 |
44 | A bundle consists of any number of source files (it may also contain
45 | other nested bundles), an output target, and a list of filters to apply.
46 |
47 | All paths are relative to your app's **static** directory, or the static
48 | directory of a :ref:`Flask blueprint `.
49 |
50 | If you prefer you can of course just as well define your assets in an
51 | external config file, and read them from there. ``webassets`` includes a
52 | number of :ref:`helper classes ` for some popular formats
53 | like YAML.
54 |
55 | Like is common for a Flask extension, a Flask-Assets instance may be used
56 | with multiple applications by initializing through ``init_app`` calls,
57 | rather than passing a fixed application object:
58 |
59 | .. code-block:: python
60 |
61 | app = Flask(__name__)
62 | assets = flask_assets.Environment()
63 | assets.init_app(app)
64 |
65 |
66 | Using the bundles
67 | ~~~~~~~~~~~~~~~~~
68 |
69 | Now with your assets properly defined, you want to merge and minify
70 | them, and include a link to the compressed result in your web page:
71 |
72 | .. code-block:: jinja
73 |
74 | {% assets "js_all" %}
75 |
76 | {% endassets %}
77 |
78 |
79 | That's it, really. **Flask-Assets** will automatically merge and compress
80 | your bundle's source files the first time the template is rendered, and will
81 | automatically update the compressed file everytime a source file changes.
82 | If you set ``ASSETS_DEBUG`` in your app configuration to ``True``, then
83 | each source file will be outputted individually instead.
84 |
85 |
86 | .. _blueprints:
87 |
88 | Flask blueprints
89 | ~~~~~~~~~~~~~~~~
90 |
91 | If you are using Flask blueprints, you can refer to a blueprint's static files
92 | via a prefix, in the same way as Flask allows you to reference a blueprint's
93 | templates:
94 |
95 | .. code-block:: python
96 |
97 | js = Bundle('app_level.js', 'blueprint/blueprint_level.js')
98 |
99 | In the example above, the bundle would reference two files,
100 | ``{APP_ROOT}/static/app_level.js``, and ``{BLUEPRINT_ROOT}/static/blueprint_level.js``.
101 |
102 | If you have used the ``webassets`` library standalone before, you may be
103 | familiar with the requirement to set the ``directory`` and ``url``
104 | configuration values. You will note that this is not required here, as
105 | Flask's static folder support is used instead. However, note that you *can*
106 | set a custom root directory or url if you prefer, for some reason. However,
107 | in this case the blueprint support of Flask-Assets is disabled, that is,
108 | referencing static files in different blueprints using a prefix, as described
109 | above, is no longer possible. All paths will be considered relative to the
110 | directory and url you specified.
111 |
112 | Pre 0.7 modules are also supported; they work exactly the same way.
113 |
114 |
115 | Templates only
116 | ~~~~~~~~~~~~~~
117 |
118 | If you prefer, you can also do without defining your bundles in code, and
119 | simply define everything inside your template:
120 |
121 | .. code-block:: jinja
122 |
123 | {% assets filters="jsmin", output="gen/packed.js",
124 | "common/jquery.js", "site/base.js", "site/widgets.js" %}
125 |
126 | {% endassets %}
127 |
128 |
129 | .. _configuration:
130 |
131 | Configuration
132 | -------------
133 |
134 | ``webassets`` supports a couple of configuration options. Those can be
135 | set both through the :class:`Environment` instance, as well as the Flask
136 | configuration. The following two statements are equivalent:
137 |
138 | .. code-block:: python
139 |
140 | assets_env.debug = True
141 | app.config['ASSETS_DEBUG'] = True
142 |
143 |
144 | For a list of available settings, see the full
145 | :ref:`webassets documentation `.
146 |
147 | Babel Configuration
148 | ~~~~~~~~~~~~~~~~~~~
149 |
150 | If you use `Babel`_ for internationalization, then you will need to
151 | add the extension to your babel configuration file
152 | as ``webassets.ext.jinja2.AssetsExtension``
153 |
154 | Otherwise, babel will not extract strings from any templates that
155 | include an ``assets`` tag.
156 |
157 | Here is an example ``babel.cfg``:
158 |
159 | .. code-block:: python
160 |
161 | [python: **.py]
162 | [jinja2: **.html]
163 | extensions=jinja2.ext.autoescape,jinja2.ext.with_,webassets.ext.jinja2.AssetsExtension
164 |
165 |
166 | .. _Babel: http://babel.edgewall.org/
167 |
168 | Flask-S3 Configuration
169 | ~~~~~~~~~~~~~~~~~~~~~~~
170 |
171 | `Flask-S3`_ allows you to upload and serve your static files from
172 | an Amazon S3 bucket. It accomplishes this by overwriting the Flask
173 | ``url_for`` function. In order for Flask-Assets to use this
174 | overwritten ``url_for`` function, you need to let it know that
175 | you are using Flask-S3. Just set
176 |
177 | .. code-block:: python
178 |
179 | app.config['FLASK_ASSETS_USE_S3']=True
180 |
181 | .. _Flask-S3: https://flask-s3.readthedocs.io/en/latest/
182 |
183 | Flask-CDN Configuration
184 | ~~~~~~~~~~~~~~~~~~~~~~~
185 |
186 | `Flask-CDN`_ allows you to upload and serve your static files from
187 | a CDN (like `Amazon Cloudfront`_), without having to modify
188 | your templates. It accomplishes this by overwriting the Flask
189 | ``url_for`` function. In order for Flask-Assets to use this
190 | overwritten ``url_for`` function, you need to let it know that
191 | you are using Flask-CDN. Just set
192 |
193 | .. code-block:: python
194 |
195 | app.config['FLASK_ASSETS_USE_CDN']=True
196 |
197 | .. _Flask-CDN: https://flask-cdn.readthedocs.io/en/latest/
198 | .. _Amazon Cloudfront: https://aws.amazon.com/cloudfront/
199 |
200 |
201 | Command Line Interface
202 | ----------------------
203 |
204 | *New in version 0.12.*
205 |
206 | Flask 0.11+ comes with build-in integration of `CLI`_ using `click`_
207 | library. The ``assets`` command is automatically installed through
208 | *setuptools* using ``flask.commands`` entry point group in **setup.py**.
209 |
210 | .. code-block:: python
211 |
212 | entry_points={
213 | 'flask.commands': [
214 | 'assets = flask_assets:assets',
215 | ],
216 | },
217 |
218 | After installing Flask 0.11+ you should see following line in the output
219 | when executing ``flask`` command in your shell:
220 |
221 | .. code-block:: console
222 |
223 | $ flask --help
224 | ...
225 | Commands:
226 | assets Web assets commands.
227 | ...
228 |
229 |
230 | .. _CLI: https://flask.pocoo.org/docs/0.11/cli/
231 | .. _click: https://click.pocoo.org/docs/latest/
232 |
233 |
234 | Using in Google App Engine
235 | ~~~~~~~~~~~~~~~~~~~~~~~~~~
236 |
237 | You can use flask-assets in Google App Engine by manually building assets.
238 | The GAE runtime cannot create files, which is necessary for normal flask-assets
239 | functionality, but you can deploy pre-built assets. You can use a file change
240 | listener to rebuild assets on the fly in development.
241 |
242 | For a fairly minimal example which includes auto-reloading in development, see
243 | `flask-assets-gae-example`_.
244 |
245 | Also see the `relevant webassets documentation`_.
246 |
247 | .. _flask-assets-gae-example: https://github.com/SocosLLC/flask-assets-gae-example
248 | .. _relevant webassets documentation: http://webassets.readthedocs.io/en/latest/faq.html#is-google-app-engine-supported
249 |
250 |
251 | API
252 | ---
253 |
254 | .. automodule:: flask_assets
255 | :members:
256 |
257 | Webassets documentation
258 | -----------------------
259 |
260 | For further information, have a look at the complete
261 | :ref:`webassets documentation `, and in particular, the
262 | following topics:
263 |
264 | - :ref:`Configuration `
265 | - :ref:`All about bundles `
266 | - :ref:`Builtin filters `
267 | - :ref:`Custom filters `
268 | - :ref:`CSS compilers `
269 | - :ref:`FAQ `
270 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | REM Command file for Sphinx documentation
4 |
5 | set SPHINXBUILD=sphinx-build
6 | set BUILDDIR=_build
7 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
8 | if NOT "%PAPER%" == "" (
9 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
10 | )
11 |
12 | if "%1" == "" goto help
13 |
14 | if "%1" == "help" (
15 | :help
16 | echo.Please use `make ^` where ^ is one of
17 | echo. html to make standalone HTML files
18 | echo. dirhtml to make HTML files named index.html in directories
19 | echo. pickle to make pickle files
20 | echo. json to make JSON files
21 | echo. htmlhelp to make HTML files and a HTML help project
22 | echo. qthelp to make HTML files and a qthelp project
23 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
24 | echo. changes to make an overview over all changed/added/deprecated items
25 | echo. linkcheck to check all external links for integrity
26 | echo. doctest to run all doctests embedded in the documentation if enabled
27 | goto end
28 | )
29 |
30 | if "%1" == "clean" (
31 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
32 | del /q /s %BUILDDIR%\*
33 | goto end
34 | )
35 |
36 | if "%1" == "html" (
37 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
38 | echo.
39 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
40 | goto end
41 | )
42 |
43 | if "%1" == "dirhtml" (
44 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
45 | echo.
46 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
47 | goto end
48 | )
49 |
50 | if "%1" == "pickle" (
51 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
52 | echo.
53 | echo.Build finished; now you can process the pickle files.
54 | goto end
55 | )
56 |
57 | if "%1" == "json" (
58 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
59 | echo.
60 | echo.Build finished; now you can process the JSON files.
61 | goto end
62 | )
63 |
64 | if "%1" == "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 | goto end
70 | )
71 |
72 | if "%1" == "qthelp" (
73 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
74 | echo.
75 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
76 | .qhcp project file in %BUILDDIR%/qthelp, like this:
77 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Flask-Assets.qhcp
78 | echo.To view the help file:
79 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Flask-Assets.ghc
80 | goto end
81 | )
82 |
83 | if "%1" == "latex" (
84 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
85 | echo.
86 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
87 | goto end
88 | )
89 |
90 | if "%1" == "changes" (
91 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
92 | echo.
93 | echo.The overview file is in %BUILDDIR%/changes.
94 | goto end
95 | )
96 |
97 | if "%1" == "linkcheck" (
98 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
99 | echo.
100 | echo.Link check complete; look for any errors in the above output ^
101 | or in %BUILDDIR%/linkcheck/output.txt.
102 | goto end
103 | )
104 |
105 | if "%1" == "doctest" (
106 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
107 | echo.
108 | echo.Testing of doctests in the sources finished, look at the ^
109 | results in %BUILDDIR%/doctest/output.txt.
110 | goto end
111 | )
112 |
113 | :end
114 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | /static/cached.css
--------------------------------------------------------------------------------
/example/app.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import sys
3 | from os import path
4 | sys.path.insert(0, path.join(path.dirname(__file__), '../src'))
5 |
6 | from flask import Flask, render_template, url_for
7 | from flask_assets import Environment, Bundle
8 |
9 | app = Flask(__name__)
10 |
11 | assets = Environment(app)
12 | assets.register('main',
13 | 'style1.css', 'style2.css',
14 | output='cached.css', filters='cssmin')
15 |
16 | @app.route('/')
17 | def index():
18 | return render_template('index.html')
19 |
20 |
21 | app.run(debug=True)
22 |
--------------------------------------------------------------------------------
/example/static/style1.css:
--------------------------------------------------------------------------------
1 | h1 {
2 | color: red;
3 | }
--------------------------------------------------------------------------------
/example/static/style2.css:
--------------------------------------------------------------------------------
1 | h2 {
2 | color: blue;
3 | }
--------------------------------------------------------------------------------
/example/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 | {% assets "main" %}
3 |
4 | {% endassets %}
5 |
6 |
7 |
This should be red.
8 |
This should be blue.
9 |
--------------------------------------------------------------------------------
/fabfile.py:
--------------------------------------------------------------------------------
1 | from fabric.api import run, put, env
2 |
3 | env.hosts = ['elsdoerfer.com:2211']
4 |
5 | def publish_docs():
6 | target = '/var/www/elsdoerfer/files/docs/flask-assets'
7 | run('rm -rf %s' % target)
8 | run('mkdir %s' % target)
9 | put('build/sphinx/html/*', '%s' % target)
--------------------------------------------------------------------------------
/requirements.in:
--------------------------------------------------------------------------------
1 | pytest
2 | webassets
3 | PyYAML
4 | pyScss
5 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | #
2 | # This file is autogenerated by pip-compile with Python 3.10
3 | # by the following command:
4 | #
5 | # pip-compile
6 | #
7 | blinker==1.6.3
8 | # via flask
9 | click==8.1.7
10 | # via flask
11 | exceptiongroup==1.1.3
12 | # via pytest
13 | flask==3.0.0
14 | # via flask-script
15 | iniconfig==2.0.0
16 | # via pytest
17 | itsdangerous==2.1.2
18 | # via flask
19 | jinja2==3.1.2
20 | # via flask
21 | markupsafe==2.1.3
22 | # via
23 | # jinja2
24 | # werkzeug
25 | packaging==23.2
26 | # via pytest
27 | pluggy==1.3.0
28 | # via pytest
29 | pyscss==1.4.0
30 | # via -r requirements.in
31 | pytest==7.4.2
32 | # via -r requirements.in
33 | pyyaml==6.0.1
34 | # via -r requirements.in
35 | six==1.16.0
36 | # via pyscss
37 | tomli==2.0.1
38 | # via pytest
39 | webassets==0.11.1
40 | # via -r requirements.in
41 | werkzeug==3.0.0
42 | # via flask
43 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 | """
4 | Flask-Assets
5 | -------------
6 |
7 | Integrates the ``webassets`` library with Flask, adding support for
8 | merging, minifying and compiling CSS and Javascript files.
9 | """
10 |
11 | from __future__ import with_statement
12 | from setuptools import setup
13 |
14 | # Figure out the version; this could be done by importing the
15 | # module, though that requires dependencies to be already installed,
16 | # which may not be the case when processing a pip requirements
17 | # file, for example.
18 | def parse_version(asignee):
19 | import os, re
20 | here = os.path.dirname(os.path.abspath(__file__))
21 | version_re = re.compile(
22 | r'%s = (\(.*?\))' % asignee)
23 | with open(os.path.join(here, 'src', 'flask_assets.py')) as fp:
24 | for line in fp:
25 | match = version_re.search(line)
26 | if match:
27 | version = eval(match.group(1))
28 | return ".".join(map(str, version))
29 | else:
30 | raise Exception("cannot find version")
31 | version = parse_version('__version__')
32 | webassets_requirement = parse_version('__webassets_version__')
33 |
34 | setup(
35 | name='Flask-Assets',
36 | version=version,
37 | url='http://github.com/miracle2k/flask-assets',
38 | license='BSD',
39 | author='Michael Elsdoerfer',
40 | author_email='michael@elsdoerfer.com',
41 | description='Asset management for Flask, to compress and merge ' \
42 | 'CSS and Javascript files.',
43 | long_description=__doc__,
44 | py_modules=['flask_assets'],
45 | package_dir={'': 'src'},
46 | zip_safe=False,
47 | platforms='any',
48 | entry_points={
49 | 'flask.commands': [
50 | 'assets = flask_assets:assets',
51 | ],
52 | },
53 | install_requires=[
54 | 'Flask>=0.8',
55 | 'webassets%s' % webassets_requirement,
56 | ],
57 | classifiers=[
58 | 'Environment :: Web Environment',
59 | 'Intended Audience :: Developers',
60 | 'License :: OSI Approved :: BSD License',
61 | 'Operating System :: OS Independent',
62 | 'Programming Language :: Python',
63 | 'Programming Language :: Python :: 3',
64 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
65 | 'Topic :: Software Development :: Libraries :: Python Modules'
66 | ],
67 | tests_require=[
68 | 'pytest',
69 | ],
70 | )
71 |
--------------------------------------------------------------------------------
/src/flask_assets.py:
--------------------------------------------------------------------------------
1 | """Integration of the ``webassets`` library with Flask."""
2 |
3 | from __future__ import print_function
4 |
5 | import logging
6 | from os import path
7 |
8 | try:
9 | from flask.globals import request_ctx, app_ctx
10 | except ImportError:
11 | from flask import _request_ctx_stack, _app_ctx_stack
12 | request_ctx = _request_ctx_stack.top
13 | app_ctx = _app_ctx_stack.top
14 | from flask import current_app, has_app_context, has_request_context
15 | from flask.templating import render_template_string
16 | # We want to expose Bundle via this module.
17 | from webassets import Bundle
18 | from webassets.env import (BaseEnvironment, ConfigStorage, Resolver,
19 | env_options, url_prefix_join)
20 | from webassets.filter import Filter, register_filter
21 | from webassets.loaders import PythonLoader, YAMLLoader
22 |
23 | __version__ = (2, 1, 1, 'dev')
24 | # webassets core compatibility used in setup.py
25 | __webassets_version__ = ('>=2.0', )
26 |
27 | __all__ = (
28 | 'Environment',
29 | 'Bundle',
30 | 'FlaskConfigStorage',
31 | 'FlaskResolver',
32 | 'Jinja2Filter',
33 | )
34 |
35 |
36 | class Jinja2Filter(Filter):
37 | """Will compile all source files as Jinja2 templates using the standard
38 | Flask contexts.
39 | """
40 | name = 'jinja2'
41 | max_debug_level = None
42 |
43 | def __init__(self, context=None):
44 | super(Jinja2Filter, self).__init__()
45 | self.context = context or {}
46 |
47 | def input(self, _in, out, source_path, output_path, **kw):
48 | out.write(render_template_string(_in.read(), **self.context))
49 |
50 | # Override the built-in ``jinja2`` filter that ships with ``webassets``. This
51 | # custom filter uses Flask's ``render_template_string`` function to provide all
52 | # the standard Flask template context variables.
53 | register_filter(Jinja2Filter)
54 |
55 |
56 | class FlaskConfigStorage(ConfigStorage):
57 | """Uses the config object of a Flask app as the backend: either the app
58 | instance bound to the extension directly, or the current Flask app on
59 | the stack.
60 |
61 | Also provides per-application defaults for some values.
62 |
63 | Note that if no app is available, this config object is basically
64 | unusable - this is by design; this could also let the user set defaults
65 | by writing to a container not related to any app, which would be used
66 | as a fallback if a current app does not include a key. However, at least
67 | for now, I specifically made the choice to keep things simple and not
68 | allow global across-app defaults.
69 | """
70 |
71 | def __init__(self, *a, **kw):
72 | self._defaults = {}
73 | ConfigStorage.__init__(self, *a, **kw)
74 |
75 | def _transform_key(self, key):
76 | if key.lower() in env_options:
77 | return "ASSETS_%s" % key.upper()
78 | else:
79 | return key.upper()
80 |
81 | def setdefault(self, key, value):
82 | """We may not always be connected to an app, but we still need
83 | to provide a way to the base environment to set it's defaults.
84 | """
85 | try:
86 | super(FlaskConfigStorage, self).setdefault(key, value)
87 | except RuntimeError:
88 | self._defaults.__setitem__(key, value)
89 |
90 | def __contains__(self, key):
91 | return self._transform_key(key) in self.env._app.config
92 |
93 | def __getitem__(self, key):
94 | value = self._get_deprecated(key)
95 | if value:
96 | return value
97 |
98 | # First try the current app's config
99 | public_key = self._transform_key(key)
100 | if self.env._app:
101 | if public_key in self.env._app.config:
102 | return self.env._app.config[public_key]
103 |
104 | # Try a non-app specific default value
105 | if key in self._defaults:
106 | return self._defaults.__getitem__(key)
107 |
108 | # Finally try to use a default based on the current app
109 | deffunc = getattr(self, "_app_default_%s" % key, False)
110 | if deffunc:
111 | return deffunc()
112 |
113 | # We've run out of options
114 | raise KeyError()
115 |
116 | def __setitem__(self, key, value):
117 | if not self._set_deprecated(key, value):
118 | self.env._app.config[self._transform_key(key)] = value
119 |
120 | def __delitem__(self, key):
121 | del self.env._app.config[self._transform_key(key)]
122 |
123 |
124 | def get_static_folder(app_or_blueprint):
125 | """Return the static folder of the given Flask app
126 | instance, or module/blueprint.
127 |
128 | In newer Flask versions this can be customized, in older
129 | ones (<=0.6) the folder is fixed.
130 | """
131 | if not hasattr(app_or_blueprint, 'static_folder'):
132 | # I believe this is for app objects in very old Flask
133 | # versions that did not support custom static folders.
134 | return path.join(app_or_blueprint.root_path, 'static')
135 |
136 | if not app_or_blueprint.has_static_folder:
137 | # Use an exception type here that is not hidden by spit_prefix.
138 | raise TypeError(('The referenced blueprint %s has no static '
139 | 'folder.') % app_or_blueprint)
140 | return app_or_blueprint.static_folder
141 |
142 |
143 | class FlaskResolver(Resolver):
144 | """Adds support for Flask blueprints.
145 |
146 | This resolver is designed to use the Flask staticfile system to
147 | locate files, by looking at directory prefixes (``foo/bar.png``
148 | looks in the static folder of the ``foo`` blueprint. ``url_for``
149 | is used to generate urls to these files.
150 |
151 | This default behaviour changes when you start setting certain
152 | standard *webassets* path and url configuration values:
153 |
154 | If a :attr:`Environment.directory` is set, output files will
155 | always be written there, while source files still use the Flask
156 | system.
157 |
158 | If a :attr:`Environment.load_path` is set, it is used to look
159 | up source files, replacing the Flask system. Blueprint prefixes
160 | are no longer resolved.
161 | """
162 |
163 | def split_prefix(self, ctx, item):
164 | """See if ``item`` has blueprint prefix, return (directory, rel_path).
165 | """
166 | app = ctx._app
167 | try:
168 | if hasattr(app, 'blueprints'):
169 | blueprint, name = item.split('/', 1)
170 | directory = get_static_folder(app.blueprints[blueprint])
171 | endpoint = '%s.static' % blueprint
172 | item = name
173 | else:
174 | # Module support for Flask < 0.7
175 | module, name = item.split('/', 1)
176 | directory = get_static_folder(app.modules[module])
177 | endpoint = '%s.static' % module
178 | item = name
179 | except (ValueError, KeyError):
180 | directory = get_static_folder(app)
181 | endpoint = 'static'
182 |
183 | return directory, item, endpoint
184 |
185 | def use_webassets_system_for_output(self, ctx):
186 | return ctx.config.get('directory') is not None or \
187 | ctx.config.get('url') is not None
188 |
189 | def use_webassets_system_for_sources(self, ctx):
190 | return bool(ctx.load_path)
191 |
192 | def search_for_source(self, ctx, item):
193 | # If a load_path is set, use it instead of the Flask static system.
194 | #
195 | # Note: With only env.directory set, we don't go to default;
196 | # Setting env.directory only makes the output directory fixed.
197 | if self.use_webassets_system_for_sources(ctx):
198 | return Resolver.search_for_source(self, ctx, item)
199 |
200 | # Look in correct blueprint's directory
201 | directory, item, endpoint = self.split_prefix(ctx, item)
202 | try:
203 | return self.consider_single_directory(directory, item)
204 | except IOError:
205 | # XXX: Hack to make the tests pass, which are written to not
206 | # expect an IOError upon missing files. They need to be rewritten.
207 | return path.normpath(path.join(directory, item))
208 |
209 | def resolve_output_to_path(self, ctx, target, bundle):
210 | # If a directory/url pair is set, always use it for output files
211 | if self.use_webassets_system_for_output(ctx):
212 | return Resolver.resolve_output_to_path(self, ctx, target, bundle)
213 |
214 | # Allow targeting blueprint static folders
215 | directory, rel_path, endpoint = self.split_prefix(ctx, target)
216 | return path.normpath(path.join(directory, rel_path))
217 |
218 | def resolve_source_to_url(self, ctx, filepath, item):
219 | # If a load path is set, use it instead of the Flask static system.
220 | if self.use_webassets_system_for_sources(ctx):
221 | return super(FlaskResolver, self).resolve_source_to_url(ctx, filepath, item)
222 |
223 | return self.convert_item_to_flask_url(ctx, item, filepath)
224 |
225 | def resolve_output_to_url(self, ctx, target):
226 | # With a directory/url pair set, use it for output files.
227 | if self.use_webassets_system_for_output(ctx):
228 | return Resolver.resolve_output_to_url(self, ctx, target)
229 |
230 | # Otherwise, behaves like all other flask URLs.
231 | return self.convert_item_to_flask_url(ctx, target)
232 |
233 | def convert_item_to_flask_url(self, ctx, item, filepath=None):
234 | """Given a relative reference like `foo/bar.css`, returns
235 | the Flask static url. By doing so it takes into account
236 | blueprints, i.e. in the aformentioned example,
237 | ``foo`` may reference a blueprint.
238 |
239 | If an absolute path is given via ``filepath``, it will be
240 | used instead. This is needed because ``item`` may be a
241 | glob instruction that was resolved to multiple files.
242 |
243 | If app.config("FLASK_ASSETS_USE_S3") exists and is True
244 | then we import the url_for function from flask_s3,
245 | otherwise we import url_for from flask directly.
246 |
247 | If app.config("FLASK_ASSETS_USE_CDN") exists and is True
248 | then we import the url_for function from flask.
249 | """
250 | if ctx.environment._app.config.get("FLASK_ASSETS_USE_S3"):
251 | try:
252 | from flask_s3 import url_for
253 | except ImportError as e:
254 | print("You must have Flask S3 to use FLASK_ASSETS_USE_S3 option")
255 | raise e
256 | elif ctx.environment._app.config.get("FLASK_ASSETS_USE_CDN"):
257 | try:
258 | from flask_cdn import url_for
259 | except ImportError as e:
260 | print("You must have Flask CDN to use FLASK_ASSETS_USE_CDN option")
261 | raise e
262 | elif ctx.environment._app.config.get("FLASK_ASSETS_USE_AZURE"):
263 | try:
264 | from flask_azure_storage import url_for
265 | except ImportError as e:
266 | print("You must have Flask Azure Storage to use FLASK_ASSETS_USE_AZURE option")
267 | raise e
268 | else:
269 | from flask import url_for
270 |
271 | directory, rel_path, endpoint = self.split_prefix(ctx, item)
272 |
273 | if filepath is not None:
274 | filename = filepath[len(directory)+1:]
275 | else:
276 | filename = rel_path
277 |
278 | # Windows compatibility
279 | filename = filename.replace("\\", "/")
280 |
281 | flask_ctx = None
282 | if not has_request_context():
283 | flask_ctx = ctx.environment._app.test_request_context()
284 | flask_ctx.push()
285 | try:
286 | url = url_for(endpoint, filename=filename)
287 | # In some cases, url will be an absolute url with a scheme and hostname.
288 | # (for example, when using werkzeug's host matching).
289 | # In general, url_for() will return a http url. During assets build, we
290 | # we don't know yet if the assets will be served over http, https or both.
291 | # Let's use // instead. url_for takes a _scheme argument, but only together
292 | # with external=True, which we do not want to force every time. Further,
293 | # this _scheme argument is not able to render // - it always forces a colon.
294 | if url and url.startswith('http:'):
295 | url = url[5:]
296 | return url
297 | finally:
298 | if flask_ctx:
299 | flask_ctx.pop()
300 |
301 |
302 | class Environment(BaseEnvironment):
303 | """This object is used to hold a collection of bundles and configuration.
304 |
305 | If it initialized with an instance of Flask application then webassets
306 | Jinja2 extension is automatically registered.
307 | """
308 |
309 | config_storage_class = FlaskConfigStorage
310 | resolver_class = FlaskResolver
311 |
312 | def __init__(self, app=None):
313 | self.app = app
314 | super(Environment, self).__init__()
315 | if app:
316 | self.init_app(app)
317 |
318 | @property
319 | def _app(self):
320 | """The application object to work with; this is either the app
321 | that we have been bound to, or the current application.
322 | """
323 | if self.app is not None:
324 | return self.app
325 |
326 | if has_request_context():
327 | return request_ctx.app
328 |
329 | if has_app_context():
330 | return app_ctx.app
331 |
332 | raise RuntimeError('assets instance not bound to an application, '+
333 | 'and no application in current context')
334 |
335 |
336 |
337 | # XXX: This is required because in a couple of places, webassets 0.6
338 | # still access env.directory, at one point even directly. We need to
339 | # fix this for 0.6 compatibility, but it might be preferable to
340 | # introduce another API similar to _normalize_source_path() for things
341 | # like the cache directory and output files.
342 | def set_directory(self, directory):
343 | self.config['directory'] = directory
344 | def get_directory(self):
345 | if self.config.get('directory') is not None:
346 | return self.config['directory']
347 | return get_static_folder(self._app)
348 | directory = property(get_directory, set_directory, doc=
349 | """The base directory to which all paths will be relative to.
350 | """)
351 | def set_url(self, url):
352 | self.config['url'] = url
353 | def get_url(self):
354 | if self.config.get('url') is not None:
355 | return self.config['url']
356 | return self._app.static_url_path
357 | url = property(get_url, set_url, doc=
358 | """The base url to which all static urls will be relative to.""")
359 |
360 | def init_app(self, app):
361 | app.jinja_env.add_extension('webassets.ext.jinja2.AssetsExtension')
362 | app.jinja_env.assets_environment = self
363 |
364 | def from_yaml(self, path):
365 | """Register bundles from a YAML configuration file"""
366 | bundles = YAMLLoader(path).load_bundles()
367 | for name in bundles:
368 | self.register(name, bundles[name])
369 |
370 | def from_module(self, path):
371 | """Register bundles from a Python module"""
372 | bundles = PythonLoader(path).load_bundles()
373 | for name in bundles:
374 | self.register(name, bundles[name])
375 |
376 |
377 | try:
378 | import click
379 | from flask import cli
380 | except ImportError:
381 | pass
382 | else:
383 | def _webassets_cmd(cmd):
384 | """Helper to run a webassets command."""
385 | from webassets.script import CommandLineEnvironment
386 | logger = logging.getLogger('webassets')
387 | logger.addHandler(logging.StreamHandler())
388 | logger.setLevel(logging.DEBUG)
389 | cmdenv = CommandLineEnvironment(
390 | current_app.jinja_env.assets_environment, logger
391 | )
392 | getattr(cmdenv, cmd)()
393 |
394 |
395 | @click.group()
396 | def assets():
397 | """Web assets commands."""
398 |
399 |
400 | @assets.command()
401 | @cli.with_appcontext
402 | def build():
403 | """Build bundles."""
404 | _webassets_cmd('build')
405 |
406 |
407 | @assets.command()
408 | @cli.with_appcontext
409 | def clean():
410 | """Clean bundles."""
411 | _webassets_cmd('clean')
412 |
413 |
414 | @assets.command()
415 | @cli.with_appcontext
416 | def watch():
417 | """Watch bundles for file changes."""
418 | _webassets_cmd('watch')
419 |
420 | __all__ = __all__ + ('assets', 'build', 'clean', 'watch')
421 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miracle2k/flask-assets/62efd23fe95ee6a86fc1cfaa98fc1e2152093557/tests/__init__.py
--------------------------------------------------------------------------------
/tests/bp_for_test/__init__.py:
--------------------------------------------------------------------------------
1 | """This is here so that the tests have a Python package available
2 | that can serve as the base for Flask blueprints used during testing.
3 | """
--------------------------------------------------------------------------------
/tests/bp_for_test/static/README:
--------------------------------------------------------------------------------
1 | static folder needs to exist for Flask to pick it up.
2 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import shutil
2 | import tempfile
3 |
4 | import pytest
5 | from flask import Flask
6 |
7 | from flask_assets import Environment
8 | from tests.helpers import new_blueprint
9 |
10 |
11 | @pytest.fixture
12 | def app():
13 | app = Flask(__name__, static_url_path="/app_static")
14 | bp = new_blueprint("bp", static_url_path="/bp_static", static_folder="static")
15 | app.register_blueprint(bp)
16 | return app
17 |
18 |
19 | @pytest.fixture
20 | def env(app):
21 | env = Environment(app)
22 | return env
23 |
24 |
25 | @pytest.fixture
26 | def no_app_env():
27 | return Environment()
28 |
29 |
30 | @pytest.fixture
31 | def temp_dir():
32 | temp = tempfile.mkdtemp()
33 | yield temp
34 | shutil.rmtree(temp, ignore_errors=True)
35 |
--------------------------------------------------------------------------------
/tests/helpers.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from flask import Blueprint
4 |
5 | __all__ = ("create_files", "new_blueprint")
6 |
7 |
8 | def create_files(parent, *files):
9 | result = []
10 | for file in files:
11 | path = os.path.join(parent, file)
12 | dir_path = os.path.dirname(path)
13 | if not os.path.exists(dir_path):
14 | os.mkdir(dir_path)
15 | f = open(path, "w", encoding="utf-8")
16 | f.close()
17 | result.append(path)
18 |
19 | return result
20 |
21 |
22 | def new_blueprint(name, import_name=None, **kwargs):
23 | if import_name is None:
24 | from tests import bp_for_test
25 | import_name = bp_for_test.__name__
26 | bp = Blueprint(name, import_name, **kwargs)
27 | return bp
28 |
--------------------------------------------------------------------------------
/tests/test_config.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from flask import Flask
3 |
4 |
5 | def test_env_set(app, env):
6 | env.url = "https://github.com/miracle2k/flask-assets"
7 | assert app.config["ASSETS_URL"] == "https://github.com/miracle2k/flask-assets"
8 |
9 |
10 | def test_env_get(app, env):
11 | app.config["ASSETS_URL"] = "https://github.com/miracle2k/flask-assets"
12 | assert env.url == "https://github.com/miracle2k/flask-assets"
13 |
14 |
15 | def test_env_config(app, env):
16 | app.config["LESS_PATH"] = "/usr/bin/less"
17 | assert env.config["LESS_PATH"] == "/usr/bin/less"
18 |
19 | with pytest.raises(KeyError):
20 | _ = env.config["do_not_exist"]
21 |
22 | assert env.config.get("do_not_exist") is None
23 |
24 |
25 | def test_no_app_env_set(no_app_env):
26 | with pytest.raises(RuntimeError):
27 | no_app_env.debug = True
28 |
29 |
30 | def test_no_app_env_get(no_app_env):
31 | with pytest.raises(RuntimeError):
32 | no_app_env.config.get("debug")
33 |
34 |
35 | def test_no_app_env_config(app, no_app_env):
36 | no_app_env.config.setdefault("foo", "bar")
37 | with app.test_request_context():
38 | assert no_app_env.config["foo"] == "bar"
39 |
40 |
41 | def test_config_isolation_within_apps(no_app_env):
42 | no_app_env.config.setdefault("foo", "bar")
43 |
44 | app1 = Flask(__name__)
45 | with app1.test_request_context():
46 | assert no_app_env.config["foo"] == "bar"
47 |
48 | no_app_env.config["foo"] = "qux"
49 | assert no_app_env.config["foo"] == "qux"
50 |
51 | app2 = Flask(__name__)
52 | with app2.test_request_context():
53 | assert no_app_env.config["foo"] == "bar"
54 |
--------------------------------------------------------------------------------
/tests/test_env.py:
--------------------------------------------------------------------------------
1 | import os
2 | import types
3 |
4 | from flask_assets import Bundle
5 |
6 |
7 | def test_assets_tag(app, env):
8 | env.register("test", "file1", "file2")
9 | template = app.jinja_env.from_string("{% assets 'test' %}{{ASSET_URL}};{% endassets %}")
10 | assert template.render() == "/app_static/file1;/app_static/file2;"
11 |
12 |
13 | def test_from_module(app, env):
14 | module = types.ModuleType("test")
15 | module.pytest = Bundle("py_file1", "py_file2")
16 | env.from_module(module)
17 | template = app.jinja_env.from_string('{% assets "pytest" %}{{ASSET_URL}};{% endassets %}')
18 | assert template.render() == '/app_static/py_file1;/app_static/py_file2;'
19 |
20 |
21 | def test_from_yaml(app, env):
22 | with open("test.yaml", "w", encoding="utf-8") as f:
23 | f.write("""
24 | yaml_test:
25 | contents:
26 | - yaml_file1
27 | - yaml_file2
28 | """)
29 | try:
30 | env.from_yaml("test.yaml")
31 | template = app.jinja_env.from_string('{% assets "yaml_test" %}{{ASSET_URL}};{% endassets %}')
32 | assert template.render() == "/app_static/yaml_file1;/app_static/yaml_file2;"
33 | finally:
34 | os.remove("test.yaml")
35 |
--------------------------------------------------------------------------------
/tests/test_integration.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 | from webassets.bundle import get_all_bundle_files
5 |
6 | from flask_assets import Bundle
7 | from tests.helpers import create_files, new_blueprint
8 |
9 |
10 | def test_directory_auto(app, env):
11 | """Test how we resolve file references through the Flask static
12 | system by default (if no custom 'env.directory' etc. values
13 | have been configured manually).
14 | """
15 | assert "directory" not in env.config
16 |
17 | assert get_all_bundle_files(Bundle("foo"), env) == [
18 | app.root_path + os.path.normpath("/static/foo")
19 | ]
20 |
21 | # Blueprints prefixes in paths are handled specifically.
22 | assert get_all_bundle_files(Bundle("bp/bar"), env) == [
23 | app.root_path + os.path.normpath("/bp_for_test/static/bar")
24 | ]
25 |
26 | # Prefixes that aren't valid blueprint names are just considered
27 | # sub-folders of the main app.
28 | assert get_all_bundle_files(Bundle("app/bar"), env) == [
29 | app.root_path + os.path.normpath("/static/app/bar")
30 | ]
31 |
32 | # In case the name of a app-level sub-folder conflicts with a
33 | # module name, you can always use this hack:
34 | assert get_all_bundle_files(Bundle("./bp_for_test/bar"), env) == [
35 | app.root_path + os.path.normpath("/static/bp_for_test/bar")
36 | ]
37 |
38 |
39 | def test_url_auto(app, env):
40 | """Test how urls are generated via the Flask static system
41 | by default (if no custom 'env.url' etc. values have been
42 | configured manually).
43 | """
44 | assert "url" not in env.config
45 |
46 | assert Bundle("foo", env=env).urls() == ["/app_static/foo"]
47 | # Urls for files that point to a blueprint use that blueprint"s url prefix.
48 | assert Bundle("bp/bar", env=env).urls() == ["/bp_static/bar"]
49 | # Try with a prefix which is not a blueprint.
50 | assert Bundle("non-bp/bar", env=env).urls() == ["/app_static/non-bp/bar"]
51 |
52 |
53 | def test_custom_load_path(app, env, temp_dir):
54 | """A custom load_path is configured - this will affect how
55 | we deal with source files.
56 | """
57 | env.append_path(temp_dir, "/custom/")
58 | create_files(temp_dir, "foo", os.path.normpath("module/bar"))
59 | assert get_all_bundle_files(Bundle("foo"), env) == [os.path.join(temp_dir, "foo")]
60 | # We do not recognize references to modules.
61 | assert get_all_bundle_files(Bundle("module/bar"), env) == [os.path.join(temp_dir, os.path.normpath("module/bar"))]
62 |
63 | assert Bundle("foo", env=env).urls() == ["/custom/foo"]
64 | assert Bundle("module/bar", env=env).urls() == ["/custom/module/bar"]
65 |
66 | # [Regression] With a load path configured, generating output
67 | # urls still works, and it still uses the flask system.
68 | env.debug = False
69 | env.url_expire = False
70 | assert Bundle("foo", output="out", env=env).urls() == ["/app_static/out"]
71 |
72 |
73 | def test_custom_directory_and_url(app, env, temp_dir):
74 | """Custom directory/url are configured - this will affect how
75 | we deal with output files."""
76 | # Create source source file, make it findable (by default,
77 | # static_folder) is set to a fixed sub-folder of the test dir (why?)
78 | create_files(temp_dir, "a")
79 | app.static_folder = temp_dir
80 | # Setup custom directory/url pair for output
81 | env.directory = temp_dir
82 | env.url = "/custom"
83 | env.debug = False # Return build urls
84 | env.url_expire = False # No query strings
85 |
86 | assert Bundle("a", output="foo", env=env).urls() == ["/custom/foo"]
87 | # We do not recognize references to modules.
88 | assert Bundle("a", output="module/bar", env=env).urls() == ["/custom/module/bar"]
89 |
90 |
91 | def test_existing_request_object_used(app, env):
92 | """Check for a bug where the url generation code of
93 | Flask-Assets always added a dummy test request to the context stack,
94 | instead of using the existing one if there is one.
95 |
96 | We test this by making the context define a custom SCRIPT_NAME
97 | prefix, and then we check if it affects the generated urls, as
98 | it should.
99 | """
100 | with app.test_request_context("/", environ_overrides={"SCRIPT_NAME": "/your_app"}):
101 | assert Bundle("foo", env=env).urls() == ["/your_app/app_static/foo"]
102 |
103 |
104 | def test_globals(app, env, temp_dir):
105 | """Make sure url generation works with globals."""
106 | app.static_folder = temp_dir
107 | create_files(temp_dir, "a.js", "b.js")
108 | b = Bundle("*.js", env=env)
109 | assert b.urls() == ["/app_static/a.js", "/app_static/b.js"]
110 |
111 |
112 | def test_blueprint_output(app, env, temp_dir):
113 | """[Regression] Output can point to a blueprint's static directory."""
114 | bp1_static_folder = (temp_dir + os.path.sep + "bp1_static")
115 | os.mkdir(bp1_static_folder)
116 |
117 | bp1 = new_blueprint("bp1", static_folder=bp1_static_folder)
118 | app.register_blueprint(bp1)
119 |
120 | app.static_folder = temp_dir
121 |
122 | with open(os.path.join(temp_dir, "foo"), "w", encoding="utf-8") as f:
123 | f.write("function bla () { /* comment */ var a; } ")
124 |
125 | Bundle("foo", filters="rjsmin", output="bp1/out", env=env).build()
126 | with open(os.path.join(bp1_static_folder, "out")) as f:
127 | assert f.read() == "function bla(){var a;}"
128 |
129 |
130 | def test_blueprint_urls(app, env):
131 | """Urls to blueprint files are generated correctly."""
132 | # source urls
133 | assert Bundle("bp/foo", env=env).urls() == ["/bp_static/foo"]
134 |
135 | # output urls - env settings are to not touch filesystem
136 | env.auto_build = False
137 | env.url_expire = False
138 | assert Bundle(output="bp/out", debug=False, env=env).urls() == ["/bp_static/out"]
139 |
140 |
141 | def test_blueprint_no_static_folder(app, env, temp_dir):
142 | """Test dealing with a blueprint without a static folder."""
143 | bp2 = new_blueprint("bp2")
144 | app.register_blueprint(bp2)
145 | with pytest.raises(TypeError):
146 | Bundle("bp2/foo", env=env).urls()
147 |
148 |
149 | def test_cssrewrite(app, env, temp_dir):
150 | """Make sure cssrewrite works with Blueprints."""
151 | bp3_static_folder = temp_dir + os.path.sep + "bp3_static"
152 | os.mkdir(bp3_static_folder)
153 | bp3 = new_blueprint("bp3", static_folder=bp3_static_folder, static_url_path="/w/u/f/f")
154 | app.register_blueprint(bp3)
155 |
156 | path = create_files(temp_dir, os.path.normpath("bp3_static/css"))[0]
157 | with open(path, "w", encoding="utf-8") as f:
158 | f.write('h1{background: url("local")}')
159 |
160 | # Source file is in a blueprint, output file is app-level.
161 | Bundle("bp3/css", filters="cssrewrite", output="out", env=env).build()
162 |
163 | # The urls are NOT rewritten using the filesystem, but
164 | # within the url space.
165 | with open(os.path.join(app.static_folder, "out"), "r") as f:
166 | assert f.read() == 'h1{background: url("../w/u/f/f/local")}'
167 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py37, py38, py39, py310, py311, py312, flask1, flask2
3 | skip_missing_interpreters = true
4 |
5 | [gh-actions]
6 | python =
7 | 3.8: py38, flask1
8 | 3.9: py39
9 | 3.10: py310, flask2
10 | 3.11: py311
11 | 3.12: py312
12 |
13 | [testenv]
14 | deps =
15 | -r requirements.txt
16 | commands =
17 | pytest
18 |
19 | [flask1]
20 | deps =
21 | -r requirements.txt
22 | flask==1.0.0
23 | commands =
24 | pytest
25 |
26 | [flask2]
27 | deps =
28 | -r requirements.txt
29 | flask==2.0.0
30 | commands =
31 | pytest
32 |
--------------------------------------------------------------------------------