├── .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 | --------------------------------------------------------------------------------