From 66be8fc22b1b54745c371fbac0ff1667436378ce Mon Sep 17 00:00:00 2001 From: Lars Holm Nielsen Date: Mon, 3 Aug 2015 12:16:12 +0200 Subject: [PATCH] global: initial package separation Signed-off-by: Lars Holm Nielsen --- .coveragerc | 24 ++ .dockerignore | 5 + .editorconfig | 31 ++ .gitignore | 57 +++ .travis.yml | 53 +++ AUTHORS | 16 + CHANGES | 6 + CONTRIBUTING.rst | 26 ++ Dockerfile | 35 ++ LICENSE | 38 ++ MANIFEST.in | 16 + README.rst | 41 +++ RELEASE-NOTES.rst | 34 ++ docker-compose.yml | 11 + docs/Makefile | 202 ++++++++++ docs/_templates/sidebarintro.html | 14 + docs/api.rst | 32 ++ docs/conf.py | 295 +++++++++++++++ docs/contributing.rst | 10 + docs/index.rst | 54 +++ docs/installation.rst | 26 ++ docs/tut_app.rst | 129 +++++++ docs/tut_ext.rst | 53 +++ docs/tut_package.rst | 88 +++++ docs/tutorial.rst | 16 + examples/manage.py | 5 + examples/myapp/__init__.py | 1 + examples/myapp/app.py | 11 + examples/myapp/cli.py | 7 + examples/myapp/config.py | 10 + examples/myapp/wsgi.py | 6 + examples/myexts/__init__.py | 0 examples/myexts/sqlalchemy.py | 30 ++ examples/mymodule/__init__.py | 0 examples/mymodule/cli.py | 18 + examples/mymodule/config.py | 3 + examples/mymodule/models.py | 8 + examples/mymodule/templates/mymodule.html | 2 + .../mymodule/templates/mymodule_base.html | 2 + examples/mymodule/views.py | 16 + examples/setup.py | 21 ++ flask_appfactory/__init__.py | 24 ++ flask_appfactory/app.py | 347 +++++++----------- flask_appfactory/cli.py | 99 +++++ flask_appfactory/ext/__init__.py | 12 + flask_appfactory/ext/jinja2.py | 77 ++++ flask_appfactory/version.py | 19 + pytest.ini | 9 + requirements.devel.txt | 3 + requirements.latest.txt | 4 + requirements.lowest.txt | 4 + run-tests.sh | 11 + setup.cfg | 14 + setup.py | 104 ++++++ tests/simplemodule/__init__.py | 0 tests/simplemodule/cli.py | 28 ++ tests/simplemodule/config.py | 1 + tests/simplemodule/templates/test.html | 1 + tests/simplemodule/views.py | 29 ++ tests/simplemodule2/__init__.py | 8 + tests/simplemodule2/templates/test.html | 1 + tests/simplemodule2/views.py | 29 ++ tests/test_app.py | 111 ++++++ tests/test_cli.py | 61 +++ tests/test_ext.py | 39 ++ tox.ini | 33 ++ 66 files changed, 2296 insertions(+), 224 deletions(-) create mode 100644 .coveragerc create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 AUTHORS create mode 100644 CHANGES create mode 100644 CONTRIBUTING.rst create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 RELEASE-NOTES.rst create mode 100644 docker-compose.yml create mode 100644 docs/Makefile create mode 100644 docs/_templates/sidebarintro.html create mode 100644 docs/api.rst create mode 100644 docs/conf.py create mode 100644 docs/contributing.rst create mode 100644 docs/index.rst create mode 100644 docs/installation.rst create mode 100644 docs/tut_app.rst create mode 100644 docs/tut_ext.rst create mode 100644 docs/tut_package.rst create mode 100644 docs/tutorial.rst create mode 100644 examples/manage.py create mode 100644 examples/myapp/__init__.py create mode 100644 examples/myapp/app.py create mode 100644 examples/myapp/cli.py create mode 100644 examples/myapp/config.py create mode 100644 examples/myapp/wsgi.py create mode 100644 examples/myexts/__init__.py create mode 100644 examples/myexts/sqlalchemy.py create mode 100644 examples/mymodule/__init__.py create mode 100644 examples/mymodule/cli.py create mode 100644 examples/mymodule/config.py create mode 100644 examples/mymodule/models.py create mode 100644 examples/mymodule/templates/mymodule.html create mode 100644 examples/mymodule/templates/mymodule_base.html create mode 100644 examples/mymodule/views.py create mode 100644 examples/setup.py create mode 100644 flask_appfactory/__init__.py create mode 100644 flask_appfactory/cli.py create mode 100644 flask_appfactory/ext/__init__.py create mode 100644 flask_appfactory/ext/jinja2.py create mode 100644 flask_appfactory/version.py create mode 100644 pytest.ini create mode 100644 requirements.devel.txt create mode 100644 requirements.latest.txt create mode 100644 requirements.lowest.txt create mode 100755 run-tests.sh create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tests/simplemodule/__init__.py create mode 100644 tests/simplemodule/cli.py create mode 100644 tests/simplemodule/config.py create mode 100644 tests/simplemodule/templates/test.html create mode 100644 tests/simplemodule/views.py create mode 100644 tests/simplemodule2/__init__.py create mode 100644 tests/simplemodule2/templates/test.html create mode 100644 tests/simplemodule2/views.py create mode 100644 tests/test_app.py create mode 100644 tests/test_cli.py create mode 100644 tests/test_ext.py create mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..7f59a2b --- /dev/null +++ b/.coveragerc @@ -0,0 +1,24 @@ +## +## This file is part of Flask-AppFactory +## Copyright (C) 2014 CERN. +## +## Flask-AppFactory is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License as +## published by the Free Software Foundation; either version 2 of the +## License, or (at your option) any later version. +## +## Flask-AppFactory is distributed in the hope that it will be useful, but +## WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Flask-AppFactory; if not, write to the Free Software Foundation, +## Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. +## +## In applying this licence, CERN does not waive the privileges and immunities +## granted to it by virtue of its status as an Intergovernmental Organization +## or submit itself to any jurisdiction. + +[run] +source = flask_appfactory \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e6c4ad9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +*.pyc +__pycache__/ +.tox +.cache diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8f95a01 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,31 @@ +root = true + +[*] +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +# Python files +[*.py] +indent_size = 4 +# isort plugin configuration +multi_line_output = 2 +default_section = THIRDPARTY + +# RST files (used by sphinx) +[*.rst] +indent_size = 4 + +# CSS, HTML, JS, JSON, YML +[*.{css,html,js,json,yml}] +indent_size = 2 + +# Matches the exact files either package.json or .travis.yml +[{package.json,.travis.yml}] +indent_size = 2 + +# Dockerfile +[Dockerfile] +indent_size = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba74660 --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d8c7c36 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,53 @@ +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +sudo: false + +language: python + +python: + - "2.7" + - "3.3" + - "3.4" + +env: + - REQUIREMENTS=devel + - REQUIREMENTS=latest + - REQUIREMENTS=lowest + +cache: + - pip + +install: + # Install test dependencies + - "travis_retry pip install coveralls pep257 Sphinx" + - "travis_retry pip install pytest pytest-pep8 pytest-cov pytest-cache" + - "travis_retry pip install -r requirements.${REQUIREMENTS}.txt" + - "travis_retry pip install -e ." + +script: + - pep257 flask_appfactory + - "sphinx-build -qnNW docs docs/_build/html" + - python setup.py test + - "sphinx-build -qnNW -b doctest docs docs/_build/doctest" + +after_success: + - coveralls + +notifications: + email: false + +deploy: + provider: pypi + user: lnielsen + password: + secure: cKcrzBcAPn5pQqhJAX+ct5L7dNDL3Y4NKt+KksWfag5YN2SCTkAEcm0JvKxP4pURG0AOXqqWjY1ydvj7XkafEOWq4lKjtJy/Eb1ZY8dO5RJOQ1fjICvtVxcW/6qQKz5TESd6CafXis0E3kmqrRAHWnFZATEhA0KnnvoCoGByZUOn9gg527dA8tv4MatY9xYQGOuryKWq/KKgD92Z9EhwwzB+26naXlpI6GQiMoxJA5br4Idc9Y5uHbJVGSxTWkQYbXo8GmYatwGmswPfJXZqJt37K8yLdYTCZku7FQ/mBNrj1OQutQh40MX72Zd3o5GEHswFLKvRcPjX4jA6kjBcGKC/Lxn2oDCKA8HhLpCUwQlhuZpXvp2Ghq8df7ZIZvsuO/KOVAske7e0L0uGcs244jY00uTFtK9X/Xtj72tBD4On/GeWXOgRW6tlwo6NsJOdR1HjiR6F3xX62IUJBXGE5obmbNTF+XciWTiY1aTKlSSRbu+M0q6jD+1DFl0ttuvdh9zMctdDkCJMqncEU5z9sDX6U08iahdHF70eCL6dRvl5aD6flbVMVOXt7z3WZPqWzLDIOCuy1JjgMyO2l++AB4lhSCdxEaOjGlLIyIBLfLnsDMtqrKavNszq4h01qlh3tiQNW9KXxXuxCTx4lr5YdQ/y6P5mhSoQBNfpZpVuRr4= + distributions: "sdist bdist_wheel" + on: + tags: true + python: "2.7" + condition: $REQUIREMENTS = latest diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..4fe87b1 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,16 @@ +Authors +======= + +Contact us at `info@invenio-software.org `_ + +* Lars Holm Nielsen +* Jiri Kuncar +* Tibor Simko +* Yoan Blanc +* Marco Neumann +* Esteban J. G. Gabancho +* Samuele Kaplun +* Konstantinos Ntemagkos +* Jan Aage Lavik +* Ivan Masár +* Adrian Tudor Panescu diff --git a/CHANGES b/CHANGES new file mode 100644 index 0000000..1f0c630 --- /dev/null +++ b/CHANGES @@ -0,0 +1,6 @@ +Changes +======= + +Version 0.1.0 (released 2015-07-31) + +- Initial public release. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..dcfba34 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,26 @@ +Contributing +============ + +Bug reports, feature requests, and other contributions are welcome. +If you find a demonstrable problem that is caused by the code of this +library, please: + +1. Search for `already reported problems + `_. +2. Check if the issue has been fixed or is still reproducible on the + latest `master` branch. +3. Create an issue with **a test case**. + +If you create a feature branch, you can run the tests to ensure everything is +operating correctly: + +.. code-block:: console + + $ ./run-tests.sh + +You can also test your feature branch using Docker: + +.. code-block:: console + + $ docker-compose build + $ docker-compose run --rm web /code/run-tests.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..792f6ae --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or modify it under +# the terms of the Revised BSD License; see LICENSE file for more details. + +# Use Python-2.7: +FROM python:2.7 + +# Install some prerequisites ahead of `setup.py` in order to profit +# from the docker build cache: +RUN pip install coveralls \ + ipython \ + pep257 \ + pytest \ + pytest-pep8 \ + pytest-cache \ + pytest-cov \ + Sphinx + +# Add sources to `code` and work there: +WORKDIR /code +ADD . /code + +# Install flask-appfactory: +RUN pip install -e . + +# Run container as user `flask-appfactory` with UID `1000`, which should match +# current host user in most situations: +RUN adduser --uid 1000 --disabled-password --gecos '' flaskappfactory && \ + chown -R flaskappfactory:flaskappfactory /code + +# Run test suite instead of starting the application: +USER flaskappfactory +CMD ["python", "setup.py", "test"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a6e8a1d --- /dev/null +++ b/LICENSE @@ -0,0 +1,38 @@ +Flask-AppFactory is free software; you can redistribute it and/or modify it +under the terms of the Revised BSD License quoted below. + +Copyright (C) 2015 CERN. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +In applying this license, CERN does not waive the privileges and immunities +granted to it by virtue of its status as an Intergovernmental Organization or +submit itself to any jurisdiction. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..b696fd2 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +include LICENSE AUTHORS CHANGES README.rst +include .coveragerc run-tests.sh +include docs/*.rst docs/*.py docs/Makefile +include tests/*.py +include examples/*.py +recursive-include docs/_themes *.py *.css *.css_t *.conf *.html README +recursive-include docs/_templates *.html diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e7eab57 --- /dev/null +++ b/README.rst @@ -0,0 +1,41 @@ +================== + Flask-AppFactory +================== + +.. image:: https://travis-ci.org/inveniosoftware/flask-appfactory.svg?branch=master + :target: https://travis-ci.org/inveniosoftware/flask-appfactory +.. image:: https://coveralls.io/repos/inveniosoftware/flask-appfactory/badge.svg?branch=master + :target: https://coveralls.io/r/inveniosoftware/flask-appfactory +.. image:: https://pypip.in/v/flask-appfactory/badge.svg + :target: https://crate.io/packages/flask-appfactory/ +.. image:: https://pypip.in/d/flask-appfactory/badge.svg + :target: https://crate.io/packages/flask-appfactory/ + +Flask-AppFactory is an dynamic application loader. + +It allows you to build reusable modules that can be easily be assembled into +full Flask applications using this loader. Each reusable module can provide +default configuration, blueprints and command line interface. + +Installation +============ +Flask-AppFactory is on PyPI so all you need is: :: + + pip install Flask-AppFactory + +Documentation +============= +Documentation is available at or can be build using Sphinx: :: + + pip install Sphinx + python setup.py build_sphinx + +Testing +======= +Running the tests are as simple as: :: + + python setup.py test + +or (to also show test coverage) :: + + ./run-tests.sh diff --git a/RELEASE-NOTES.rst b/RELEASE-NOTES.rst new file mode 100644 index 0000000..3b5bc9b --- /dev/null +++ b/RELEASE-NOTES.rst @@ -0,0 +1,34 @@ +========================= + Flask-AppFactory v0.1.0 +========================= + +Flask-AppFactory v0.1.0 was released on July 31, 2015. + +About +----- + +Flask-AppFactory is an application loader which assembles an Flask +applications from individually released modules based on configuration. + +Installation +------------ + + $ pip install flask-appfactory + +Documentation +------------- + + http://flask-appfactory.readthedocs.org/en/v0.1.0 + +Homepage +-------- + + https://github.com/inveniosoftware/flask-appfactory + +Happy hacking and thanks for flying Flask-AppFactory. + +| Invenio Development Team +| Email: info@invenio-software.org +| Twitter: http://twitter.com/inveniosoftware +| GitHub: http://github.com/inveniosoftware +| URL: http://invenio-software.org diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..13024d7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or modify it under +# the terms of the Revised BSD License; see LICENSE file for more details. + +web: + build: . + command: python setup.py test + volumes: + - .:/code diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..a961a8a --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Flask-AppFactory +## Copyright (C) 2014 CERN. +## +## Flask-AppFactory is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License as +## published by the Free Software Foundation; either version 2 of the +## License, or (at your option) any later version. +## +## Flask-AppFactory is distributed in the hope that it will be useful, but +## WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Flask-AppFactory; if not, write to the Free Software Foundation, +## Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. +## +## In applying this licence, CERN does not waive the privileges and immunities +## granted to it by virtue of its status as an Intergovernmental Organization +## or submit itself to any jurisdiction. + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo + @echo "Build finished. The coverage pages are in $(BUILDDIR)/coverage/python.txt." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-AppFactory.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-AppFactory.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-AppFactory" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-AppFactory" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html new file mode 100644 index 0000000..9c0bcc0 --- /dev/null +++ b/docs/_templates/sidebarintro.html @@ -0,0 +1,14 @@ +

About

+

+ Flask-AppFactory is an extension for Flask that CHANGEME +

+

Useful Links

+ + +Fork me on GitHub \ No newline at end of file diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..984ac4a --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,32 @@ +.. _api: + +API Docs +======== + + +Application factory +------------------- +.. automodule:: flask_appfactory.app + :members: + :undoc-members: + +CLI factory +----------- +.. automodule:: flask_appfactory.cli + :members: + :undoc-members: + +Extensions +---------- + +.. automodule:: flask_appfactory.ext + :members: + :undoc-members: + +Jinja2 +~~~~~~ + +.. automodule:: flask_appfactory.ext.jinja2 + :members: + :undoc-members: + diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..e0e6cef --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,295 @@ +# -*- coding: utf-8 -*- +# +# Flask-AppFactory documentation build configuration file, created by +# sphinx-quickstart on Fri Jul 31 13:10:12 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os +import shlex +import re + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.viewcode', +] + +# Add any paths that contain templates here, relative to this directory. +# templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Flask-AppFactory' +copyright = u'2015, CERN' +author = u'Invenio Software' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +# Get the version string. Cannot be done with import! +with open(os.path.join('..', 'flask_appfactory', 'version.py'), 'rt') as f: + version = re.search( + '__version__\s*=\s*"(?P.*)"\n', + f.read() + ).group('version') + +# The full version, including alpha/beta/rc tags. +release = version + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +#html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Flask-AppFactorydoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'Flask-AppFactory.tex', u'Flask-AppFactory Documentation', + u'Invenio Software', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'Flask-AppFactory', u'Flask-AppFactory Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'Flask-AppFactory', u'Flask-AppFactory Documentation', + author, 'Flask-AppFactory', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000..39a5163 --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1,10 @@ +.. include:: ../CHANGES + +.. include:: ../CONTRIBUTING.rst + +License +======= + +.. include:: ../LICENSE + +.. include:: ../AUTHORS diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..7b1413b --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,54 @@ +================== + Flask-AppFactory +================== +.. currentmodule:: flask_appfactory + +.. raw:: html + +

+ + travis-ci badge + + + coveralls.io badge + +

+ +.. automodule:: flask_appfactory + +User's Guide +============ + +This part of the documentation will show you how to get started in using +Flask-AppFactory with Flask. + +.. toctree:: + :maxdepth: 2 + + installation + tutorial + + +API Reference +============= + +If you are looking for information on a specific function, class or +method, this part of the documentation is for you. + +.. toctree:: + :maxdepth: 2 + + api + + +Additional Notes +================ + +Notes on how to contribute, legal information and changes are here for the interested. + +.. toctree:: + :maxdepth: 2 + + contributing diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..e07e430 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,26 @@ +.. _installation: + +Installation +============ + +Install Flask-AppFactory with ``pip`` :: + + pip install flask-appfactory + +The development version can be downloaded from `its page at GitHub +`_. :: + + git clone https://github.com/inveniosoftware/flask-appfactory.git + cd flask-appfactory + python setup.py develop + ./run-tests.sh + +**Requirements** + +Flask-AppFactory has the following dependencies: + +* `Flask `_ +* `Flask-Registry `_ +* `Flask-CLI `_ + +Flask-AppFactory requires Python version 2.7 or 3.3+ diff --git a/docs/tut_app.rst b/docs/tut_app.rst new file mode 100644 index 0000000..1816525 --- /dev/null +++ b/docs/tut_app.rst @@ -0,0 +1,129 @@ +Step 3: Creating an application +=============================== + +You now have a reusable package and an extension. The last step is to make +it into a fully functioning Flask application, and the step where +Flask-AppFactory will do all the heavy lifting for you. + +Here's how the directory structure looks like:: + + myapp/__init__.py (empty) + myapp/app.py + myapp/cli.py + myapp/config.py + myapp/wsgi.py + setup.py + + +Application configuration +------------------------- +First we define how our application is assembled by providing the default +application configuration: + +.. literalinclude:: ../examples/myapp/config.py + :language: python + :linenos: + +The ``PACKAGES`` defines the list of resuable packages that +Flask-AppFactory should load. The order of packages is important as templates, +translations etc. is loaded according this order. + +The ``EXTENSIONS`` defines the list of extensions that Flask-AppFactory +should load. You'll see that in addition to ``myexts.sqlalchemy`` we also +load ``flask_appfactory.ext.jinja2``. + + +Application factory +------------------- +Next, we create our Flask `application factory `_, +by using the ``flask_appfactory.appfactory()``. The ``appfactory`` +method is passed: + +- the name of the Flask application (line 7). +- the Python import path of our application configuration (line 8). +- whether to load just configuration or the entire application (line 9). +- and optional keyword arguments which will be merged with the configuration + (line 10). + +.. literalinclude:: ../examples/myapp/app.py + :language: python + :linenos: + +The application factory will take care of loading all extensions and reusable +packages. In addition to the configuration provided in line 8, the factory +will try to load configuration from: + + 1. Instance folder (``/.cfg``). + 2. The keyword arguments (``**kwargs_config``). + 3. Environment variables. + +This allows you to have configuration for test/production environments or +ingesting configuration into the Flask application in e.g. docker containers. + +Command Line Interface factory +------------------------------ +Next, we create the CLI for our application that we can use to manage the +application and run e.g. the development server: + +.. literalinclude:: ../examples/myapp/cli.py + :language: python + :linenos: + +We simply import our application factory (line 5) and pass it to the +``flask_appfactory.clifactory()`` method (line 7). + +The actual management script we install with an entry point in the package's +``setup.py`` (line 13): + +.. literalinclude:: ../examples/setup.py + :language: python + :linenos: + :emphasize-lines: 11-15 + +The new management script will besides your applications commands also have two +commands to 1) run a development server and 2) start a interactive Python shell +with the Flask application context. + +.. code-block:: console + + $ myapp + Usage: myapp [OPTIONS] COMMAND [ARGS]... + + Options: + --help Show this message and exit. + + Commands: + initdb Initialize database. + run Runs a development server. + shell Runs a shell in the app context. + testapp Command with application context. + testsimple Command without application context. + +The command ``initdb`` was provided by our extension, while ``testapp`` and +``testsimple`` is provided by our reusable package. + +.. note:: + It is also possible to use the ``flask`` command instead of creating a + custom script. This can be achieved by creating a file ``manage.py``: + + .. literalinclude:: ../examples/manage.py + :language: python + :linenos: + + Next, export ``FLASK_APP`` environment variable and point it to your + manage.py file: + + .. code-block:: console + + $ export FLASK_APP=/path/to/manage.py + $ flask initdb + +WSGI Application +---------------- + +Last but not least, you will likely need a WSGI file to run your service in +production environment: + +.. literalinclude:: ../examples/myapp/wsgi.py + :language: python + :linenos: diff --git a/docs/tut_ext.rst b/docs/tut_ext.rst new file mode 100644 index 0000000..e26666b --- /dev/null +++ b/docs/tut_ext.rst @@ -0,0 +1,53 @@ +Step 2: Enabling an extension +----------------------------- + +In step 1 you have created your first reusable package. Usually however, your +packages will need access to e.g. a database backend, a cache etc. These +features are enabled via extensions. + +Flask-SQLAlchemy extension +~~~~~~~~~~~~~~~~~~~~~~~~~~ +Next up in our tutorial, we will enable the +`Flask-SQLAlchemy `_ +extension to provide database models for our application and reusable packages. + +Here's how the directory structure looks like:: + + myext/__init__.py (empty) + myext/sqlalchemy.py + +Flask-AppFactory enables extensions simply by calling a method +``setup_app(app)`` with the Flask application object (line 16). This +pattern allows each extension to customize the Flask application as they see +fit. + +.. literalinclude:: ../examples/myexts/sqlalchemy.py + :language: python + :linenos: + :emphasize-lines: 16, 18, 10 + +Each extension can set default configuration (line 18). The extension should +however not overwrite an existing value, hence ``app.config.setdefault`` +is used. This is because any instance specific configuration has already been +loaded at this point. + +By default Flask-AppFactory only loads configuration, blueprints and CLI from +each reusable package. Each enabled extension can however load further modules +from each package. E.g. in the Flask-SQLAlchemy case, we may want to load +database models from a ``models.py`` in all enabled reusable packages. This is +achieved by creating a registry (line 10). + +Each extension can natually also provide CLI commands (see line 26-30). + + +Using Flask-SQLAlchemy in packages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now that the extension has been enabled, you'll want to use it in your +packages. E.g. you could add a ``models.py`` file to the reusable package +you created in step 1: + +.. literalinclude:: ../examples/mymodule/models.py + :language: python + :linenos: + diff --git a/docs/tut_package.rst b/docs/tut_package.rst new file mode 100644 index 0000000..1e14dfb --- /dev/null +++ b/docs/tut_package.rst @@ -0,0 +1,88 @@ +Step 1: Creating a reusable package +----------------------------------- +First, let's start by creating a small reusable package that we would possibly +like to use in many different applications. The package consists of: + +- a Flask blueprint + templates. +- a Click command line interface. +- default configuration for the package. + +Here's how the directory structure looks like:: + + mymodule/__init__.py (empty) + mymodule/cli.py + mymodule/config.py + mymodule/views.py + mymodule/templates/mymodule.html + mymodule/templates/mymodule_base.html + + +Default configuration +~~~~~~~~~~~~~~~~~~~~~ +The package can provide default configuration values in a ``config.py`` that it +expects to be set. The values are merged into the Flask applications +configuration and can be overwritten by each instance of an application. + +.. literalinclude:: ../examples/mymodule/config.py + :language: python + :linenos: + +Blueprint +~~~~~~~~~ +The package can also provide a blueprint. The blueprint will automatically be +registered on the Flask application by Flask-AppFactory. + + +.. literalinclude:: ../examples/mymodule/views.py + :language: python + :linenos: + :emphasize-lines: 8, 15 + +This example blueprint simply renders the template ``mymodule.html`` and +pass it the value of ``MYMODULE_GREETING`` (line 15) that we ensured was set in +``config.py``. Notice, also that the Blueprint's template folder is set in line +8 (without it, the templates in the next section is not found). + + +Templates +~~~~~~~~~ +The package provides two templates ``mymodule.html`` and ``mymodule_base.html`` +, where ``mymodule.html`` simply extends ``mymodule_base.html``. The reason for +this slightly odd method, is that it allows other packages to easily modify the +templates without copy/pasting the entire template code. Another package simply +creates a ``mymodule.html`` also extending from ``mymodule_base.html``, and +only overwrites the few template blocks that it needs to customize. + +.. literalinclude:: ../examples/mymodule/templates/mymodule.html + :language: html + :linenos: + +.. literalinclude:: ../examples/mymodule/templates/mymodule_base.html + :language: html + :linenos: + +.. note:: + It is usually a good idea to put your templates into a subfolder to avoid + name conflicts between multiple packages. + + +Command Line interface +~~~~~~~~~~~~~~~~~~~~~~ + +Finally, our package provide some simple CLI commands in ``cli.py`` that will +be merged into a single CLI application that can used to manage a Flask +application. + +The CLI is based on the `Click `_ Python package which +has good extensive documentation. + +.. literalinclude:: ../examples/mymodule/cli.py + :language: python + :linenos: + :emphasize-lines: 18, 13 + +Flask-AppFactory expects to find a variable ``commands`` in ``cli.py`` +with a list of commands to register (line 18). Using the +``@with_appcontext`` (line 13) decorator the commands can access the +Flask application context (e.g ``current_app``). Without the decorator +the application is not fully loaded in order to speed up the CLI. diff --git a/docs/tutorial.rst b/docs/tutorial.rst new file mode 100644 index 0000000..e6e1d9a --- /dev/null +++ b/docs/tutorial.rst @@ -0,0 +1,16 @@ +Tutorial +======== + + +You want to develop applications and modules based on Flask-AppFactory? Here +is a simple example walk through, to get an idea how it looks like. + +The full source code by viewed in the ``examples`` folder in the package or on +`GitHub `_. + +.. toctree:: + :maxdepth: 1 + + tut_package + tut_ext + tut_app diff --git a/examples/manage.py b/examples/manage.py new file mode 100644 index 0000000..d94c4c5 --- /dev/null +++ b/examples/manage.py @@ -0,0 +1,5 @@ +# manage.py +from myapp.app import create_app +from flask_appfactory import load_cli +app = create_app() +load_cli(app) diff --git a/examples/myapp/__init__.py b/examples/myapp/__init__.py new file mode 100644 index 0000000..ba3a4b0 --- /dev/null +++ b/examples/myapp/__init__.py @@ -0,0 +1 @@ +"""Example Flask application using Flask-AppFactory.""" diff --git a/examples/myapp/app.py b/examples/myapp/app.py new file mode 100644 index 0000000..efc0a80 --- /dev/null +++ b/examples/myapp/app.py @@ -0,0 +1,11 @@ +# myapp/app.py + +from flask_appfactory import appfactory + +def create_app(load=True, **kwargs_config): + return appfactory( + "myapp", + "myapp.config", + load=load, + **kwargs_config + ) diff --git a/examples/myapp/cli.py b/examples/myapp/cli.py new file mode 100644 index 0000000..fff4025 --- /dev/null +++ b/examples/myapp/cli.py @@ -0,0 +1,7 @@ +# myapp/cli.py + +from __future__ import absolute_import +from flask_appfactory.cli import clifactory +from .app import create_app + +cli = clifactory(create_app) diff --git a/examples/myapp/config.py b/examples/myapp/config.py new file mode 100644 index 0000000..94f49c0 --- /dev/null +++ b/examples/myapp/config.py @@ -0,0 +1,10 @@ +# myapp/config.py + +EXTENSIONS = [ + "flask_appfactory.ext.jinja2", + "myexts.sqlalchemy", +] + +PACKAGES = [ + "mymodule", +] diff --git a/examples/myapp/wsgi.py b/examples/myapp/wsgi.py new file mode 100644 index 0000000..75a5664 --- /dev/null +++ b/examples/myapp/wsgi.py @@ -0,0 +1,6 @@ +# myapp/wsgi.py + +from __future__ import absolute_import +from .app import create_app + +application = create_app() diff --git a/examples/myexts/__init__.py b/examples/myexts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/myexts/sqlalchemy.py b/examples/myexts/sqlalchemy.py new file mode 100644 index 0000000..8fc6de3 --- /dev/null +++ b/examples/myexts/sqlalchemy.py @@ -0,0 +1,30 @@ +# myexts/sqlalchemy.py + +import click +from flask_cli import with_appcontext +from flask.ext.sqlalchemy import SQLAlchemy +from flask_registry import ModuleAutoDiscoveryRegistry, RegistryProxy + +db = SQLAlchemy() + +models = RegistryProxy( + 'models', # Registry namespace + ModuleAutoDiscoveryRegistry, + 'models' # Module name (i.e. models.py) +) + +def setup_app(app): + # Set default configuration + app.config.setdefault( + 'SQLALCHEMY_DATABASE_URI', + 'sqlite:////tmp/test.db' + ) + # Add extension CLI to application. + app.cli.add_command(initdb) + db.init_app(app) + +@click.command() +@with_appcontext +def initdb(): + """Initialize database.""" + db.create_all() diff --git a/examples/mymodule/__init__.py b/examples/mymodule/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/mymodule/cli.py b/examples/mymodule/cli.py new file mode 100644 index 0000000..a32b0f0 --- /dev/null +++ b/examples/mymodule/cli.py @@ -0,0 +1,18 @@ +# mymodule/cli.py + +import click +from flask import current_app +from flask_cli import with_appcontext + +@click.command() +def testsimple(): + """Command without application context.""" + click.echo("Test") + +@click.command() +@with_appcontext +def testapp(): + """Command with application context.""" + click.echo(current_app.name) + +commands = [testsimple, testapp] diff --git a/examples/mymodule/config.py b/examples/mymodule/config.py new file mode 100644 index 0000000..53dcb7b --- /dev/null +++ b/examples/mymodule/config.py @@ -0,0 +1,3 @@ +# mymodule/config.py + +MYMODULE_GREETING = "Hello, World!" diff --git a/examples/mymodule/models.py b/examples/mymodule/models.py new file mode 100644 index 0000000..a1dde73 --- /dev/null +++ b/examples/mymodule/models.py @@ -0,0 +1,8 @@ +# mymodule/models.py + +from myexts.sqlalchemy import db + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True) + email = db.Column(db.String(120), unique=True) diff --git a/examples/mymodule/templates/mymodule.html b/examples/mymodule/templates/mymodule.html new file mode 100644 index 0000000..0249dfb --- /dev/null +++ b/examples/mymodule/templates/mymodule.html @@ -0,0 +1,2 @@ +{# mymodule/templates/mymodule.html #} +{% extends "mymodule_base.html" %} diff --git a/examples/mymodule/templates/mymodule_base.html b/examples/mymodule/templates/mymodule_base.html new file mode 100644 index 0000000..196f977 --- /dev/null +++ b/examples/mymodule/templates/mymodule_base.html @@ -0,0 +1,2 @@ +{# mymodule/templates/mymodule_base.html #} +{% block body %}{{greeting}}{% endblock %} diff --git a/examples/mymodule/views.py b/examples/mymodule/views.py new file mode 100644 index 0000000..acb056d --- /dev/null +++ b/examples/mymodule/views.py @@ -0,0 +1,16 @@ +# mymodule/views.py + +from flask import Blueprint, current_app, render_template + +blueprint = Blueprint( + 'mymodule', + __name__, + template_folder='templates', +) + +@blueprint.route("/") +def index(): + return render_template( + 'mymodule.html', + greeting=current_app.config['MYMODULE_GREETING'], + ) diff --git a/examples/setup.py b/examples/setup.py new file mode 100644 index 0000000..8f2d9d4 --- /dev/null +++ b/examples/setup.py @@ -0,0 +1,21 @@ +# setup.py + +from setuptools import setup + +setup( + name='MyApp', + version="1.0", + packages=['myapp', "myexts", "mymodule"], + zip_safe=False, + include_package_data=True, + entry_points={ + 'console_scripts': [ + 'myapp = myapp.cli:cli', + ] + }, + install_requires=[ + 'Flask>=0.10', + 'Flask-AppFactory', + 'Flask-SQLAlchemy', + ], +) diff --git a/flask_appfactory/__init__.py b/flask_appfactory/__init__.py new file mode 100644 index 0000000..c9057b1 --- /dev/null +++ b/flask_appfactory/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +"""Flask-AppFactory is an dynamic application loader. + +It allows you to build reusable modules that can be easily be assembled into +full Flask applications using this loader. Each reusable module can provide +default configuration, blueprints and command line interface. +""" + +from __future__ import absolute_import, unicode_literals, print_function + +from .app import appfactory +from .cli import clifactory, load_cli + +from .version import __version__ + +__all__ = ('appfactory', 'clifactory', 'load_cli', '__version__') diff --git a/flask_appfactory/app.py b/flask_appfactory/app.py index 72e85d4..92dbfe8 100644 --- a/flask_appfactory/app.py +++ b/flask_appfactory/app.py @@ -1,199 +1,160 @@ # -*- coding: utf-8 -*- # -# This file is part of Invenio. -# Copyright (C) 2011, 2012, 2013, 2014, 2015 CERN. +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. # -# Invenio is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 2 of the -# License, or (at your option) any later version. -# -# Invenio is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Invenio; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. -"""Implements the application factory.""" +"""Flask application factory.""" -from __future__ import absolute_import +from __future__ import absolute_import, print_function, unicode_literals -import ast +import logging import os import re import sys -import urllib import warnings -from flask_registry import ( - BlueprintAutoDiscoveryRegistry, - ConfigurationRegistry, - ExtensionRegistry, - PackageRegistry, - Registry -) - -from pkg_resources import iter_entry_points - -from six.moves.urllib.parse import urlparse - -from werkzeug.local import LocalProxy - -from .helpers import unicodifier, with_app_context -from .utils import captureWarnings -from .wrappers import Flask - - -__all__ = ('create_app', 'with_app_context') - +import ast +from flask import Flask +from flask_cli import FlaskCLI +from flask_registry import BlueprintAutoDiscoveryRegistry, \ + ConfigurationRegistry, ExtensionRegistry, PackageRegistry, Registry -class WSGIScriptAliasFix(object): - """WSGI ScriptAlias fix middleware. +def configure_warnings(): + """Configure warnings by routing warnings to the logging system. - It relies on the fact that the ``WSGI_SCRIPT_ALIAS`` environment variable - exists in the Apache configuration and identifies the virtual path to - the invenio application. + It also unhides ``DeprecationWarning``. + """ + if not sys.warnoptions: + # Route warnings through python logging + logging.captureWarnings(True) - This setup will first look for the present of a file on disk. If the file - exists, it will serve it otherwise it calls the WSGI application. + # DeprecationWarning is by default hidden, hence we force the + # "default" behavior on deprecation warnings which is not to hide + # errors. + warnings.simplefilter("default", DeprecationWarning) - If no ``WSGI_SCRIPT_ALIAS`` is defined, it does not alter anything. - .. code-block:: apacheconf +def load_config(app, module_name, **kwargs_config): + """Load configuration. - SetEnv WSGI_SCRIPT_ALIAS /wsgi - WSGIScriptAlias /wsgi /opt/invenio/invenio/invenio.wsgi + Configuration is loaded in the following order: - RewriteEngine on - RewriteCond %{REQUEST_FILENAME} !-f - RewriteRule ^(.*)$ /wsgi$1 [PT,L] + 1. Configuration module (i.e. ``module_name``). + 2. Instance configuration in ``/.cfg`` + 3. Keyword configuration arguments. + 4. Environment variables specified in ``_APP_CONFIG_ENVS`` + configuration variable or comma separated list in environment variable + with the same name. - .. seealso:: + Additionally checks if ``SECRET_KEY`` is set in the configuration and warns + if it is not. - `modwsgi Configuration Guidelines - `_ + :param app: Flask application. + :param module_name: Configuration module. + :param kwargs_config: Configuration keyword arguments """ + # 1. Load site specific default configuration + app.config.from_object(module_name) - def __init__(self, app): - """Initialize wsgi app wrapper.""" - self.app = app - - def __call__(self, environ, start_response): - """Parse path from ``REQUEST_URI`` to fix ``PATH_INFO``.""" - if environ.get('WSGI_SCRIPT_ALIAS') == environ['SCRIPT_NAME']: - path_info = urllib.unquote_plus( - urlparse(environ.get('REQUEST_URI')).path - ) # addresses issue with url encoded arguments in Flask routes - environ['SCRIPT_NAME'] = '' - environ['PATH_INFO'] = path_info - return self.app(environ, start_response) - - -def cleanup_legacy_configuration(app): - """Cleanup legacy issue in configuration.""" - from .i18n import language_list_long - # Invenio is all using str objects. Let's change them to unicode - app.config.update(unicodifier(dict(app.config))) - # ... and map certain common parameters - app.config['CFG_LANGUAGE_LIST_LONG'] = LocalProxy(language_list_long) - app.config['CFG_WEBDIR'] = app.static_folder + # 2. Load .cfg from instance folder + app.config.from_pyfile('{0}.cfg'.format(app.name), silent=True) + # 3. Update application config from parameters. + app.config.update(kwargs_config) -def register_legacy_blueprints(app): - """Register some legacy blueprints.""" - @app.route('/testing') - def testing(): - from flask import render_template - return render_template('404.html') + # 4. Update config with specified environment variables. + envvars = '{0}_APP_CONFIG_ENVS'.format(app.name.upper()) + for cfg_name in app.config.get(envvars, os.getenv(envvars, '')).split(','): + cfg_name = cfg_name.strip().upper() + if cfg_name: + cfg_value = app.config.get(cfg_name) + cfg_value = os.getenv(cfg_name, cfg_value) + try: + cfg_value = ast.literal_eval(cfg_value) + except (SyntaxError, ValueError): + pass + app.config[cfg_name] = cfg_value + app.logger.debug("{0} = {1}".format(cfg_name, cfg_value)) -def register_secret_key(app): - """Register sercret key in application configuration.""" - SECRET_KEY = app.config.get('SECRET_KEY') or \ - app.config.get('CFG_SITE_SECRET_KEY', 'change_me') + # Ensure SECRET_KEY is set. + SECRET_KEY = app.config.get('SECRET_KEY') - if not SECRET_KEY or SECRET_KEY == 'change_me': - fill_secret_key = """ - Set variable SECRET_KEY with random string in invenio.cfg. + if SECRET_KEY is None: + app.config["SECRET_KEY"] = 'change_me' + warnings.warn( + "Set variable SECRET_KEY with random string in {}".format( + os.path.join(app.instance_path, "{}.cfg".format(app.name)), + ), UserWarning) - You can use following commands: - $ %s - """ % ('inveniomanage config create secret-key', ) - warnings.warn(fill_secret_key, UserWarning) + # Initialize application registry, used for discovery and loading of + # configuration, extensions and blueprints + Registry(app=app) - app.config["SECRET_KEY"] = SECRET_KEY + app.extensions['registry'].update( + # Register packages listed in PACKAGES conf variable. + packages=PackageRegistry(app)) + app.extensions['loaded'] = False -def load_site_config(app): - """Load default site-configuration via entry points.""" - entry_points = list(iter_entry_points("invenio.config")) - if len(entry_points) > 1: - warnings.warn( - "Found multiple site configurations. This may lead to unexpected " - "results.", - UserWarning - ) - for ep in entry_points: - app.config.from_object(ep.module_name) +def load_application(app): + """Load the application. + Assembles the application by use of ``PACKAGES`` and ``EXTENSIONS`` + configuration variables. -def configure_warnings(): - """Configure warnings by routing warnings to the logging system. + 1. Load extensions by calling ``setup_app()`` in module defined in + ``EXTENSIONS``. + 2. Register blueprints from each module defined in ``PACAKGES`` by looking + searching in ``views.py`` for a ``blueprint`` or ``blueprints`` + variable. - It also unhides DeprecationWarning. + :param app: Flask application. """ - if not sys.warnoptions: - # Route warnings through python logging - captureWarnings(True) - - # DeprecationWarning is by default hidden, hence we force the - # "default" behavior on deprecation warnings which is not to hide - # errors. - warnings.simplefilter("default", DeprecationWarning) + # Extend application config with default configuration values from packages + # (app config takes precedence) + ConfigurationRegistry(app) + app.extensions['registry'].update( + # Register extensions listed in EXTENSIONS conf variable. + extensions=ExtensionRegistry(app), + # Register blueprints from packages in PACKAGES configuration variable. + blueprints=BlueprintAutoDiscoveryRegistry(app=app), + ) -def create_app(instance_path=None, static_folder=None, **kwargs_config): - """Prepare Invenio application based on Flask. + app.extensions['loaded'] = True - Invenio consists of a new Flask application with legacy support for - the old WSGI legacy application and the old Python legacy - scripts (URLs to ``*.py`` files). - For configuration variables detected from environment variables, a prefix - will be used which is the uppercase version of the app name, excluding - any non-alphabetic ('[^A-Z]') characters. +def base_app(app_name, instance_path=None, static_folder=None, + static_url_path='/static/', instance_relative_config=True, + template_folder='templates', + **kwargs): + """Create a base Flask Application. - If `instance_path` is `None`, the `_INSTANCE_PATH` environment - variable will be used. If that one does not exist, a path inside - `sys.prefix` will be used. + Ensures instance path and is set and created. Instance path defaults to + ``/var/-instance``. - .. versionadded:: 2.2 - If `static_folder` is `None`, the `_STATIC_FOLDER` environment - variable will be used. If that one does not exist, a path inside the - detected `instance_path` will be used. + Additionally configure warnings to be routed to the Python logging system, + and by default makes ``DeprecationWarning`` loud. """ configure_warnings() - # Flask application name - app_name = '.'.join(__name__.split('.')[0:2]) - # Prefix for env variables env_prefix = re.sub('[^A-Z]', '', app_name.upper()) # Detect instance path instance_path = instance_path or \ os.getenv(env_prefix + '_INSTANCE_PATH') or \ - os.path.join( - sys.prefix, 'var', app_name + '-instance' - ) + os.path.join(sys.prefix, 'var', app_name + '-instance') # Detect static files path - static_folder = static_folder or \ + static_folder = instance_path or \ os.getenv(env_prefix + '_STATIC_FOLDER') or \ os.path.join(instance_path, 'static') @@ -207,96 +168,34 @@ def create_app(instance_path=None, static_folder=None, **kwargs_config): # Create the Flask application instance app = Flask( app_name, - # Static files are usually handled directly by the webserver (e.g. - # Apache) However in case WSGI is required to handle static files too - # (such as when running simple server), then this flag can be - # turned on (it is done automatically by wsgi_handler_test). - # We assume anything under '/' which is static to be server directly - # by the webserver from CFG_WEBDIR. In order to generate independent - # url for static files use func:`url_for('static', filename='test')`. - static_url_path='', - static_folder=static_folder, - template_folder='templates', - instance_relative_config=True, + static_url_path=static_url_path, + static_folder=static_folder or os.path.join(instance_path, 'static'), + instance_relative_config=instance_relative_config, instance_path=instance_path, + template_folder=template_folder, ) - # Handle both URLs with and without trailing slashes by Flask. - # @blueprint.route('/test') - # @blueprint.route('/test/') -> not necessary when strict_slashes == False - app.url_map.strict_slashes = False - - # - # Configuration loading - # - - # Load default configuration - app.config.from_object('invenio.base.config') - - # Load site specific default configuration from entry points - load_site_config(app) - - # Load invenio.cfg from instance folder - app.config.from_pyfile('invenio.cfg', silent=True) - - # Update application config from parameters. - app.config.update(kwargs_config) - - # Ensure SECRET_KEY has a value in the application configuration - register_secret_key(app) - - # Update config with specified environment variables. - for cfg_name in app.config.get('INVENIO_APP_CONFIG_ENVS', - os.getenv('INVENIO_APP_CONFIG_ENVS', - '').split(',')): - cfg_name = cfg_name.strip().upper() - if cfg_name: - cfg_value = app.config.get(cfg_name) - cfg_value = os.getenv(cfg_name, cfg_value) - try: - cfg_value = ast.literal_eval(cfg_value) - except (SyntaxError, ValueError): - pass - app.config[cfg_name] = cfg_value - app.logger.debug("{0} = {1}".format(cfg_name, cfg_value)) - - # ==================== - # Application assembly - # ==================== - # Initialize application registry, used for discovery and loading of - # configuration, extensions and Invenio packages - Registry(app=app) - - app.extensions['registry'].update( - # Register packages listed in invenio.cfg - packages=PackageRegistry(app)) - - app.extensions['registry'].update( - # Register extensions listed in invenio.cfg - extensions=ExtensionRegistry(app), - # Register blueprints - blueprints=BlueprintAutoDiscoveryRegistry(app=app), - ) - - # Extend application config with configuration from packages (app config - # takes precedence) - ConfigurationRegistry(app) + # Compatibility layer to support Flask 1.0 click integration on v0.10 + FlaskCLI(app=app) - # Legacy conf cleanup - cleanup_legacy_configuration(app) + return app - register_legacy_blueprints(app) - return app +def appfactory(app_name, module_name, load=True, **kwargs_config): + """Create a Flask application according to a defined configuration. + :param app_name: Flask application name. + :param module_name: Python configuration module. + :param load: Load application (instead of only the configuration). + Default: ``True``. + :param kwargs_config: Extra configuration variables for the Flask + application. + """ + app = base_app(app_name) -def create_wsgi_app(*args, **kwargs): - """Create WSGI application.""" - app = create_app(*args, **kwargs) + load_config(app, module_name, **kwargs_config) - if app.debug: - from werkzeug.debug import DebuggedApplication - app.wsgi_app = DebuggedApplication(app.wsgi_app, evalex=True) + if load: + load_application(app) - app.wsgi_app = WSGIScriptAliasFix(app.wsgi_app) return app diff --git a/flask_appfactory/cli.py b/flask_appfactory/cli.py new file mode 100644 index 0000000..7bf9050 --- /dev/null +++ b/flask_appfactory/cli.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +"""Command line interface factory.""" + +from __future__ import absolute_import, print_function, unicode_literals + +import click +from flask_cli import FlaskGroup +from flask_registry import ModuleAutoDiscoveryRegistry + +from .app import load_application + + +class CLIDiscoveryRegistry(ModuleAutoDiscoveryRegistry): + + """Discover CLI modules and register them on a command collection. + + Searches for a variable ``commands`` in a module ``cli`` in each package. + The variable must be a list of commands/groups to register, e.g: + + .. code-block:: python + + import click + + @click.command() + def testcmd(): + click.echo("Test") + + commands = [testcmd, ] + + :param cli: A ``click.Command`` or ``click.Group`` object. + :param app: Flask application. + """ + + def __init__(self, cli, app, **kwargs): + """Initialize the registry.""" + self.cli = cli + super(CLIDiscoveryRegistry, self).__init__('cli', app=app, **kwargs) + + def register(self, module): + """Register modules with CLI variable.""" + module_commands = getattr(module, 'commands', None) + if module_commands is not None: + for c in module_commands: + if isinstance(c, click.BaseCommand): + self.cli.add_command(c) + + super(CLIDiscoveryRegistry, self).register(module) + + +def load_cli(app, cli=None): + """Load CLI commands and register them on CLI application. + + :param app: Flask application instance. + :param cli: Click command group. If no group is provided, the commands are + registered on the Flask applications cli. + """ + CLIDiscoveryRegistry(app.cli if cli is None else cli, app) + + +def clifactory(create_app, **config): + """Create a click CLI application based on configuration. + + The CLI will install the default ``run`` and ``shell`` commands from Flask, + and load commands from the list of modules defined in ``PACKAGES``. It will + search in ``cli.py`` in each module for a variable ``cli``. + + The Flask application is not fully loaded unless the Flask app context is + required. + + :param create_app: Flask application factory function. + """ + # Create application object without loading the full application. + app = create_app(load=False, **config) + + def create_cli_app(info): + if not app.extensions['loaded']: + load_application(app) + return app + + @click.group(cls=FlaskGroup, create_app=create_cli_app) + def cli(**params): + pass + + # # Create command collection + # cli = AppFactoryCollection(create_cli_app) + # cli.add_source(flask_cli) + + # Register CLI modules from packages. + load_cli(app, cli) + + return cli diff --git a/flask_appfactory/ext/__init__.py b/flask_appfactory/ext/__init__.py new file mode 100644 index 0000000..26d6c50 --- /dev/null +++ b/flask_appfactory/ext/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +"""Extensions for Flask-AppFactory.""" + +from __future__ import absolute_import, unicode_literals, print_function diff --git a/flask_appfactory/ext/jinja2.py b/flask_appfactory/ext/jinja2.py new file mode 100644 index 0000000..7d19ce8 --- /dev/null +++ b/flask_appfactory/ext/jinja2.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +"""Order-aware Jinja2 loader and extensions initialization. + +The default Flask Jinja2 loader is not aware of the order defined in +``PACKAGES``. This means that if two modules provides the same template, it is +undefined which template is being rendered. This extension adds a +``PACKAGES`` order-aware Jinja2 loader, which will search for a given template +in each module in the order defined by ``PACKAGES``. This allows modules to +override templates defined in modules later in ``PACKAGES``. + + +Additionally the extension will load any Jinja2 extension defined in the +``JINJA2_EXTENSIONS`` configuration variable. +""" + +from __future__ import absolute_import, print_function, unicode_literals + +from distutils.version import LooseVersion +from flask import __version__ as flask_version +from flask.templating import DispatchingJinjaLoader +from jinja2 import ChoiceLoader + +# Flask 1.0 changes return value of _iter_loaders so for compatibility with +# both Flask 0.10 and 1.0 we here check the version. +# See Flask commit bafc13981002dee4610234c7c97ac176766181c1 +IS_FLASK_1_0 = LooseVersion(flask_version) >= LooseVersion("0.11-dev") + +try: + # Deprecated in Flask commit 817b72d484d353800d907b3580c899314bf7f3c6 + from flask.templating import blueprint_is_module +except ImportError: + def blueprint_is_module(blueprint): + """Dummy function for Flask 1.0.""" + return False + + +class OrderAwareDispatchingJinjaLoader(DispatchingJinjaLoader): + + """Order aware dispatching Jinja loader. + + Customization of default Flask Jinja2 template loader. By default the + Flask Jinja2 template loader is not aware of the order of Blueprints as + defined by the ``PACKAGES`` configuration variable. + """ + + def _iter_loaders(self, template): + for blueprint in self.app.extensions['registry']['blueprints']: + if blueprint_is_module(blueprint): + continue + loader = blueprint.jinja_loader + if loader is not None: + if IS_FLASK_1_0: + yield blueprint, loader + else: + yield loader, template + + +def setup_app(app): + """Initialize Jinja2 loader and extensions.""" + # Customize Jinja loader. + jinja_loader = ChoiceLoader([ + OrderAwareDispatchingJinjaLoader(app), + app.jinja_loader, + ]) + app.jinja_loader = jinja_loader + + # Load Jinja extensions + for ext in app.config.get('JINJA2_EXTENSIONS', []): + app.jinja_env.add_extension(ext) diff --git a/flask_appfactory/version.py b/flask_appfactory/version.py new file mode 100644 index 0000000..c2f47dc --- /dev/null +++ b/flask_appfactory/version.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +"""Version information for Flask-AppFactory. + +This file is imported by ``flask_appfactory.__init__``, and parsed by +``setup.py`` as well as ``docs/conf.py``. +""" + +# Do not change the format of this next line. Doing so risks breaking +# setup.py and docs/conf.py + +__version__ = "0.1.0.dev20150731" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..2c895a9 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,9 @@ +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +[pytest] +addopts = --clearcache --pep8 --ignore=docs --cov=flask_appfactory --cov-report=term-missing tests flask_appfactory diff --git a/requirements.devel.txt b/requirements.devel.txt new file mode 100644 index 0000000..f717467 --- /dev/null +++ b/requirements.devel.txt @@ -0,0 +1,3 @@ +-e git+https://github.com/mitsuhiko/flask.git#egg=Flask +-e git+https://github.com/inveniosoftware/flask-registry.git#egg=Flask-Registry +-e git+https://github.com/inveniosoftware/flask-cli.git#egg=Flask-CLI diff --git a/requirements.latest.txt b/requirements.latest.txt new file mode 100644 index 0000000..0142252 --- /dev/null +++ b/requirements.latest.txt @@ -0,0 +1,4 @@ +Flask +click +Flask-Registry +Flask-CLI diff --git a/requirements.lowest.txt b/requirements.lowest.txt new file mode 100644 index 0000000..10993b7 --- /dev/null +++ b/requirements.lowest.txt @@ -0,0 +1,4 @@ +Flask==0.10 +click==2.0 +Flask-Registry==0.2.0 +Flask-CLI==0.2.0 diff --git a/run-tests.sh b/run-tests.sh new file mode 100755 index 0000000..a0e3308 --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,11 @@ +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +pep257 flask_appfactory && \ +sphinx-build -qnNW docs docs/_build/html && \ +python setup.py test && \ +sphinx-build -qnNW -b doctest docs docs/_build/doctest diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..4fd8d2d --- /dev/null +++ b/setup.cfg @@ -0,0 +1,14 @@ +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +[wheel] +universal=1 + +[build_sphinx] +source-dir = docs/ +build-dir = docs/_build +all_files = 1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ed91794 --- /dev/null +++ b/setup.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +"""Flask-AppFactory is an dynamic application loader.""" + +import os +import re +import sys + +from setuptools import setup +from setuptools.command.test import test as TestCommand + + +class PyTest(TestCommand): + + """Integration of PyTest with setuptools.""" + + user_options = [('pytest-args=', 'a', 'Arguments to pass to py.test')] + + def initialize_options(self): + """Initialize options.""" + TestCommand.initialize_options(self) + try: + from ConfigParser import ConfigParser + except ImportError: + from configparser import ConfigParser + config = ConfigParser() + config.read("pytest.ini") + self.pytest_args = config.get("pytest", "addopts").split(" ") + + def finalize_options(self): + """Finalize options.""" + TestCommand.finalize_options(self) + self.test_args = [] + self.test_suite = True + + def run_tests(self): + """Run tests.""" + # import here, cause outside the eggs aren't loaded + import pytest + import _pytest.config + pm = _pytest.config.get_plugin_manager() + pm.consider_setuptools_entrypoints() + errno = pytest.main(self.pytest_args) + sys.exit(errno) + +# Get the version string. Cannot be done with import! +with open(os.path.join('flask_appfactory', 'version.py'), 'rt') as f: + version = re.search( + '__version__\s*=\s*"(?P.*)"\n', + f.read() + ).group('version') + +tests_require = [ + 'pytest-cache>=1.0', + 'pytest-cov>=1.8.0', + 'pytest-pep8>=1.0.6', + 'pytest>=2.6.1', + 'coverage<4.0a1', +] + +setup( + name='Flask-AppFactory', + version=version, + description=__doc__, + url='http://github.com/inveniosoftware/flask-appfactory/', + license='BSD', + author='Invenio Collaboration', + author_email='info@invenio-software.org', + long_description=open('README.rst').read(), + packages=['flask_appfactory', ], + zip_safe=False, + platforms='any', + tests_require=tests_require, + install_requires=[ + 'Flask>=0.10', + 'Flask-Registry>=0.2.0', + 'Flask-CLI>=0.2.0', + ], + extras_require={ + 'docs': ['sphinx_rtd_theme'], + }, + cmdclass={'test': PyTest}, + classifiers=[ + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Environment :: Web Environment', + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Topic :: Utilities', + ], +) diff --git a/tests/simplemodule/__init__.py b/tests/simplemodule/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/simplemodule/cli.py b/tests/simplemodule/cli.py new file mode 100644 index 0000000..494f2d5 --- /dev/null +++ b/tests/simplemodule/cli.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +import click +from flask import current_app +from flask_cli import with_appcontext + + +@click.command() +def testsimple(): + """.""" + click.echo("Test Simple") + + +@click.command() +@with_appcontext +def testapp(): + """.""" + click.echo("Test %s" % current_app.name) + + +commands = [testsimple, testapp] diff --git a/tests/simplemodule/config.py b/tests/simplemodule/config.py new file mode 100644 index 0000000..2c02a97 --- /dev/null +++ b/tests/simplemodule/config.py @@ -0,0 +1 @@ +SIMPLEMODULE_VAR = True diff --git a/tests/simplemodule/templates/test.html b/tests/simplemodule/templates/test.html new file mode 100644 index 0000000..5330d5c --- /dev/null +++ b/tests/simplemodule/templates/test.html @@ -0,0 +1 @@ +SIMPLEMODULE diff --git a/tests/simplemodule/views.py b/tests/simplemodule/views.py new file mode 100644 index 0000000..c985306 --- /dev/null +++ b/tests/simplemodule/views.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +from __future__ import absolute_import, unicode_literals, print_function + +from flask import Blueprint + +blueprint = Blueprint( + 'simplemodule', + __name__, + template_folder='templates', + static_folder='static' +) + + +@blueprint.route("/") +def index(): + return 'TEST' + + +@blueprint.route("/simplemodule") +def template_test(): + return render_template('test.html') diff --git a/tests/simplemodule2/__init__.py b/tests/simplemodule2/__init__.py new file mode 100644 index 0000000..651f535 --- /dev/null +++ b/tests/simplemodule2/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. diff --git a/tests/simplemodule2/templates/test.html b/tests/simplemodule2/templates/test.html new file mode 100644 index 0000000..65bcd50 --- /dev/null +++ b/tests/simplemodule2/templates/test.html @@ -0,0 +1 @@ +SIMPLEMODULE2 diff --git a/tests/simplemodule2/views.py b/tests/simplemodule2/views.py new file mode 100644 index 0000000..e62664c --- /dev/null +++ b/tests/simplemodule2/views.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +from __future__ import absolute_import, unicode_literals, print_function + +from flask import Blueprint, render_template + +blueprint = Blueprint( + 'simplemodule2', + __name__, + template_folder='templates', + static_folder='static' +) + + +@blueprint.route("/") +def index(): + return 'TEST' + + +@blueprint.route("/simplemodule2") +def template_test(): + return render_template('test.html') diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..71c55f2 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +"""Test app factory.""" + +from __future__ import absolute_import, print_function, unicode_literals + +import os +from flask_appfactory import appfactory + + +def test_dummy_app(): + """.""" + class conf: + SOMEVAR = True + app = appfactory("dummyapp", conf) + assert app.name == "dummyapp" + assert app.config['SOMEVAR'] + assert app.config['SECRET_KEY'] == "change_me" + assert len(app.extensions['registry']['packages']) == 0 + assert len(app.extensions['registry']['blueprints']) == 0 + + with app.test_client() as c: + rv = c.get('/') + assert rv.status_code == 404 + + +def test_conf_overwrite(): + """.""" + class conf: + SOMEVAR = True + app = appfactory("dummyapp", conf, SOMEVAR=False) + assert app.config['SOMEVAR'] == False + + +def test_envvars_overwrite(): + """.""" + class conf: + SOMEVAR = True + + try: + os.environ['MYAPP_APP_CONFIG_ENVS'] = 'SOMEVAR, SOMEVAR1' + os.environ['SOMEVAR'] = 'False' + os.environ['SOMEVAR1'] = '"V1"' + os.environ['SOMEVAR2'] = '"V2"' + + app = appfactory("myapp", conf, SOMEVAR=True) + # Env overrides kwargs + assert app.config['SOMEVAR'] == False + # Only vars specified in MYAPP_APP_CONFIG_ENVS is set. + assert app.config['SOMEVAR1'] == "V1" + assert 'SOMEVAR2' not in app.config + finally: + del os.environ['MYAPP_APP_CONFIG_ENVS'] + del os.environ['SOMEVAR'] + del os.environ['SOMEVAR1'] + del os.environ['SOMEVAR2'] + + +def test_envvars_string(): + """.""" + class conf: + pass + + try: + os.environ['MYAPP_APP_CONFIG_ENVS'] = 'SOMEVAR' + os.environ['SOMEVAR'] = 'syntaxerror' + app = appfactory("myapp", conf) + assert app.config['SOMEVAR'] == "syntaxerror" + finally: + del os.environ['MYAPP_APP_CONFIG_ENVS'] + del os.environ['SOMEVAR'] + + +def test_simple_app(): + """.""" + class conf: + PACKAGES = ['simplemodule'] + app = appfactory("simpleapp", conf) + + assert app.extensions['loaded'] + assert len(app.extensions['registry']['packages']) == 1 + assert len(app.extensions['registry']['blueprints']) == 1 + assert app.config['SIMPLEMODULE_VAR'] + + with app.test_client() as c: + rv = c.get('/') + assert rv.status_code == 200 + assert rv.data == 'TEST'.encode('utf-8') + + +def test_simple_app_noload(): + """.""" + class conf: + PACKAGES = ['simplemodule'] + app = appfactory("simpleapp", conf, load=False) + + assert not app.extensions['loaded'] + assert len(app.extensions['registry']['packages']) == 1 + assert 'blueprints' not in app.extensions['registry'] + assert 'SIMPLEMODULE_VAR' not in app.config + assert 'PACKAGES' in app.config + + with app.test_client() as c: + assert c.get('/').status_code == 404 diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..b844601 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +"""Test app factory.""" + +from __future__ import absolute_import, print_function, unicode_literals + +from click.testing import CliRunner +from flask import Flask +from flask_appfactory import appfactory, clifactory +from flask_cli import ScriptInfo + + +def create_app(load=True): + """Application factory used for testing.""" + class conf: + PACKAGES = ['simplemodule'] + + return appfactory('myapp', conf, load=load) + + +def test_factory(): + """Test CLI factory method with/without app context.""" + cli = clifactory(create_app) + + # Without app context + runner = CliRunner() + result = runner.invoke(cli, ['testsimple']) + assert result.exit_code == 0 + assert result.output == 'Test Simple\n' + + # With app context + runner = CliRunner() + result = runner.invoke(cli, ['testapp']) + assert result.exit_code == 0 + assert result.output == 'Test myapp\n' + + +def test_cli_module_only_testing(): + """Test that CLI module can be tested standalone.""" + from simplemodule.cli import testsimple, testapp + + runner = CliRunner() + result = runner.invoke(testsimple, []) + assert result.exit_code == 0 + assert result.output == 'Test Simple\n' + + # Testing click applications which needs the Flask app context requires you + # to manually create a ScriptInfo object. + obj = ScriptInfo(create_app=lambda info: Flask('anotherapp')) + + runner = CliRunner() + result = runner.invoke(testapp, [], obj=obj) + assert result.exit_code == 0 + assert result.output == 'Test anotherapp\n' diff --git a/tests/test_ext.py b/tests/test_ext.py new file mode 100644 index 0000000..e0ecca5 --- /dev/null +++ b/tests/test_ext.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +"""Test app factory.""" + +from __future__ import absolute_import, print_function, unicode_literals + +from flask_appfactory import appfactory + + +def test_jinja2_ext(): + """Test Jinja2 extension.""" + class conf: + PACKAGES = ['simplemodule', 'simplemodule2'] + EXTENSIONS = ['flask_appfactory.ext.jinja2'] + JINJA2_EXTENSIONS = ['jinja2.ext.do'] + + app = appfactory("dummyapp", conf) + + with app.test_client() as c: + c.get('/simplemodule').data == "SIMPLEMODULE" + c.get('/simplemodule2') == "SIMPLEMODULE" + + # Test reverse package order + class conf: + PACKAGES = ['simplemodule2', 'simplemodule'] + EXTENSIONS = ['flask_appfactory.ext.jinja2'] + + app = appfactory("dummyapp", conf) + + with app.test_client() as c: + c.get('/simplemodule').data == "SIMPLEMODULE2" + c.get('/simplemodule2') == "SIMPLEMODULE2" diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..3c04756 --- /dev/null +++ b/tox.ini @@ -0,0 +1,33 @@ +# This file is part of Flask-AppFactory +# Copyright (C) 2015 CERN. +# +# Flask-AppFactory is free software; you can redistribute it and/or +# modify it under the terms of the Revised BSD License; see LICENSE +# file for more details. + +# +# Tox configuration file +# + +[tox] +envlist = {py27,py33,py34}-{lowest,latest,devel} + +[testenv] +commands = + python setup.py test + +deps = + pytest + + lowest: Flask==0.10 + lowest: click==2.0 + lowest: Flask-CLI==0.2.0 + lowest: Flask-Registry==0.2.0 + latest: Flask + latest: click + latest: Flask-CLI + latest: Flask-Registry + devel: git+https://github.com/mitsuhiko/flask.git + devel: git+https://github.com/mitsuhiko/click.git + devel: git+https://github.com/inveniosoftware/flask-cli.git + devel: git+https://github.com/inveniosoftware/flask-registry.git