././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1767221582.0146005 flask_flatpages-0.9.0/0000755000175100017510000000000015125324516014320 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1767221582.0146005 flask_flatpages-0.9.0/Flask_FlatPages.egg-info/0000755000175100017510000000000015125324516021000 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221581.0 flask_flatpages-0.9.0/Flask_FlatPages.egg-info/PKG-INFO0000644000175100017510000000456615125324515022107 0ustar00runnerrunnerMetadata-Version: 2.4 Name: Flask-FlatPages Version: 0.9.0 Summary: Provides flat static pages to a Flask application Author: Simon Sapin, Igor Davydenko, Padraic Calpin Maintainer-email: Padraic Calpin Project-URL: Repository, https://github.com/Flask-FlatPages/Flask-FlatPages Project-URL: Documentation, https://flask-flatpages.readthedocs.io/en/latest/ Classifier: Environment :: Web Environment Classifier: Framework :: Flask Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Requires-Python: >=3.8 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: Flask>1.0 Requires-Dist: Jinja2>=2.10.2 Requires-Dist: Markdown>=2.5 Requires-Dist: PyYAML>5.3.1 Provides-Extra: codehilite Requires-Dist: Pygments>=2.5.2; extra == "codehilite" Dynamic: license-file =============== Flask-FlatPages =============== .. image:: https://github.com/flask-FlatPages/flask-FlatPages/actions/workflows/test.yml/badge.svg?branch=master :target: https://github.com/flask-FlatPages/flask-FlatPages/actions/workflows/test.yml/ .. image:: https://img.shields.io/pypi/v/Flask-FlatPages.svg :target: https://pypi.python.org/pypi/Flask-FlatPages Provides flat static pages to a Flask_ application, based on text files as opposed to a relational database. * Works on Python 3.8+ * BSD licensed * Latest documentation `on Read the Docs`_ * Source, issues and pull requests `on Github`_ * Releases `on PyPI`_ * Install with ``pip install Flask-FlatPages`` .. _Flask: http://flask.pocoo.org/ .. _on Read the Docs: http://flask-flatpages.readthedocs.org/ .. _on Github: https://github.com/SimonSapin/Flask-FlatPages/ .. _on PyPI: http://pypi.python.org/pypi/Flask-FlatPages ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221582.0 flask_flatpages-0.9.0/Flask_FlatPages.egg-info/SOURCES.txt0000644000175100017510000000110115125324516022655 0ustar00runnerrunnerLICENSE MANIFEST.in README.rst pyproject.toml setup.py Flask_FlatPages.egg-info/PKG-INFO Flask_FlatPages.egg-info/SOURCES.txt Flask_FlatPages.egg-info/dependency_links.txt Flask_FlatPages.egg-info/requires.txt Flask_FlatPages.egg-info/top_level.txt docs/Makefile docs/conf.py docs/index.rst docs/requirements.txt flask_flatpages/__init__.py flask_flatpages/flatpages.py flask_flatpages/imports.py flask_flatpages/page.py flask_flatpages/utils.py flask_flatpages/parsers/__init__.py flask_flatpages/parsers/yaml.py tests/test_flask_flatpages.py tests/test_markdown_extensions.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221581.0 flask_flatpages-0.9.0/Flask_FlatPages.egg-info/dependency_links.txt0000644000175100017510000000000115125324515025045 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221581.0 flask_flatpages-0.9.0/Flask_FlatPages.egg-info/requires.txt0000644000175100017510000000012215125324515023372 0ustar00runnerrunnerFlask>1.0 Jinja2>=2.10.2 Markdown>=2.5 PyYAML>5.3.1 [codehilite] Pygments>=2.5.2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221581.0 flask_flatpages-0.9.0/Flask_FlatPages.egg-info/top_level.txt0000644000175100017510000000002015125324515023521 0ustar00runnerrunnerflask_flatpages ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/LICENSE0000644000175100017510000000302315125324470015322 0ustar00runnerrunnerCopyright (c) 2010-2014 by Simon Sapin, 2013-2014 by Igor Davydenko. Some 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. * The names of the contributors may not 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 OWNER 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/MANIFEST.in0000644000175100017510000000024615125324470016057 0ustar00runnerrunnerinclude LICENSE recursive-include docs * recursive-exclude docs *.pyc prune docs/_build prune docs/_themes/.git recursive-include tests recursive-exclude tests *.pyc ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1767221582.0146005 flask_flatpages-0.9.0/PKG-INFO0000644000175100017510000000456615125324516015430 0ustar00runnerrunnerMetadata-Version: 2.4 Name: Flask-FlatPages Version: 0.9.0 Summary: Provides flat static pages to a Flask application Author: Simon Sapin, Igor Davydenko, Padraic Calpin Maintainer-email: Padraic Calpin Project-URL: Repository, https://github.com/Flask-FlatPages/Flask-FlatPages Project-URL: Documentation, https://flask-flatpages.readthedocs.io/en/latest/ Classifier: Environment :: Web Environment Classifier: Framework :: Flask Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Requires-Python: >=3.8 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: Flask>1.0 Requires-Dist: Jinja2>=2.10.2 Requires-Dist: Markdown>=2.5 Requires-Dist: PyYAML>5.3.1 Provides-Extra: codehilite Requires-Dist: Pygments>=2.5.2; extra == "codehilite" Dynamic: license-file =============== Flask-FlatPages =============== .. image:: https://github.com/flask-FlatPages/flask-FlatPages/actions/workflows/test.yml/badge.svg?branch=master :target: https://github.com/flask-FlatPages/flask-FlatPages/actions/workflows/test.yml/ .. image:: https://img.shields.io/pypi/v/Flask-FlatPages.svg :target: https://pypi.python.org/pypi/Flask-FlatPages Provides flat static pages to a Flask_ application, based on text files as opposed to a relational database. * Works on Python 3.8+ * BSD licensed * Latest documentation `on Read the Docs`_ * Source, issues and pull requests `on Github`_ * Releases `on PyPI`_ * Install with ``pip install Flask-FlatPages`` .. _Flask: http://flask.pocoo.org/ .. _on Read the Docs: http://flask-flatpages.readthedocs.org/ .. _on Github: https://github.com/SimonSapin/Flask-FlatPages/ .. _on PyPI: http://pypi.python.org/pypi/Flask-FlatPages ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/README.rst0000644000175100017510000000160415125324470016007 0ustar00runnerrunner=============== Flask-FlatPages =============== .. image:: https://github.com/flask-FlatPages/flask-FlatPages/actions/workflows/test.yml/badge.svg?branch=master :target: https://github.com/flask-FlatPages/flask-FlatPages/actions/workflows/test.yml/ .. image:: https://img.shields.io/pypi/v/Flask-FlatPages.svg :target: https://pypi.python.org/pypi/Flask-FlatPages Provides flat static pages to a Flask_ application, based on text files as opposed to a relational database. * Works on Python 3.8+ * BSD licensed * Latest documentation `on Read the Docs`_ * Source, issues and pull requests `on Github`_ * Releases `on PyPI`_ * Install with ``pip install Flask-FlatPages`` .. _Flask: http://flask.pocoo.org/ .. _on Read the Docs: http://flask-flatpages.readthedocs.org/ .. _on Github: https://github.com/SimonSapin/Flask-FlatPages/ .. _on PyPI: http://pypi.python.org/pypi/Flask-FlatPages ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1767221582.0126004 flask_flatpages-0.9.0/docs/0000755000175100017510000000000015125324516015250 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/docs/Makefile0000644000175100017510000001521615125324470016714 0ustar00runnerrunner# Makefile for Sphinx documentation # # 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." 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-LazyViews.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-LazyViews.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-LazyViews" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-LazyViews" @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." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/docs/conf.py0000644000175100017510000001607015125324470016552 0ustar00runnerrunner# -*- coding: utf-8 -*- # # Flask-FlatPages documentation build configuration file, created by # sphinx-quickstart on Fri Dec 24 15:20:25 2010. # # 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 os import re # -- 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.intersphinx', 'pallets_sphinx_themes', 'reno.sphinxext'] # Intersphinx Mapping intersphinx_mapping = {'flask': ('http://flask.pocoo.org/docs/', None)} # Add any paths that contain templates here, relative to this directory. # templates_path = ['_templates'] # The suffix of source filenames. 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 = 'Flask-FlatPages' copyright = '2010-2015, Simon Sapin. 2013-2015, Igor Davydenko' # 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. # release = re.search('__version__ = "([^\']+)"', open(os.path.join( os.path.dirname(__file__), '..', 'flask_flatpages', '__init__.py')) .read()).group(1) # The short X.Y version. #version = '0.1' version = re.split('[a-zA-Z]', release)[0] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #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 = [] # -- 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 = 'flask' # 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 = {} html_theme_options = { 'index_sidebar_logo': False, } # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ['_themes'] # 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'] # 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 # Output file base name for HTML help builder. htmlhelp_basename = 'Flask-FlatPagesdoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Flask-FlatPages.tex', 'Flask-FlatPages Documentation', 'Simon Sapin', '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 # Additional stuff for the LaTeX preamble. #latex_preamble = '' # 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 = [ ('index', 'flask-flatpages', 'Flask-FlatPages Documentation', ['Simon Sapin', 'Igor Davydenko', 'Padraic Calpin'], 1) ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/docs/index.rst0000644000175100017510000004034415125324470017115 0ustar00runnerrunnerFlask-FlatPages =============== Flask-FlatPages provides a collection of pages to your `Flask`_ application. Pages are built from “flat” text files as opposed to a relational database. * Works on Python 3.8+ * BSD licensed * Latest documentation on `Read the Docs`_ * Source, issues and pull requests `Github`_ * Releases on `PyPI`_ .. _Flask: http://flask.pocoo.org/ .. _Read the Docs: http://flask-flatpages.readthedocs.org/ .. _Github: https://github.com/SimonSapin/Flask-FlatPages/ .. _PyPI: http://pypi.python.org/pypi/Flask-FlatPages Installation ------------ Install the extension with `pip `_:: $ pip install Flask-FlatPages or you can get the `source code from github `_. Configuration ------------- To get started all you need to do is to instantiate a :class:`FlatPages` object after configuring the application:: from flask import Flask from flask_flatpages import FlatPages app = Flask(__name__) app.config.from_pyfile('mysettings.cfg') pages = FlatPages(app) you can also pass the Flask application object later, by calling :meth:`~.FlatPages.init_app`:: pages = FlatPages() def create_app(config='mysettings.cfg'): app = Flask(__name__) app.config.from_pyfile(config) pages.init_app(app) return app Flask-FlatPages accepts the following configuration values. All of them are optional. ``FLATPAGES_ROOT`` Path to the directory where to look for page files. If relative, interpreted as relative to the application root, next to the ``static`` and ``templates`` directories. Defaults to ``pages``. ``FLATPAGES_INSTANCE_RELATIVE`` .. versionadded:: 0.7 If `True`, Flask-Flatpages will interpret the root as relative to the application's `instance folder `_. Defaults to `False`. ``FLATPAGES_EXTENSION`` Filename extension for pages. Files in the ``FLATPAGES_ROOT`` directory without this suffix are ignored. Defaults to ``.html``. .. versionchanged:: 0.6 Support multiple file extensions via sequences, e.g.: ``['.htm', '.html']`` or via comma-separated strings: ``.htm,.html``. ``FLATPAGES_CASE_INSENSITIVE`` .. versionadded:: 0.7 If `True`, the path property of each :class:`Page` instance will be all lower case. Flask-Flatpages will throw a `ValueError` if multiple pages would correspond to the same path. ``FLATPAGES_ENCODING`` Encoding of the pages files. Defaults to ``utf8``. ``FLATPAGES_HTML_RENDERER`` Callable or import string for a callable that takes at least the unicode body of a page, and return its HTML rendering as a unicode string. Defaults to :func:`~.pygmented_markdown`. .. versionchanged:: 0.5 Support for passing the :class:`~.FlatPages` instance as second argument. .. versionchanged:: 0.6 Support for passing the :class:`~.Page` instance as third argument. Renderer functions need to have at least one argument, the unicode body. The use of either :class:`~FlatPages` as second argument or :class:`~FlatPages` and :class:`Page` as second respective third argument is optional, and allows for more advanced renderers. ``FLATPAGES_MARKDOWN_EXTENSIONS`` .. versionadded:: 0.4 List of Markdown extensions to use with default HTML renderer, given as either 'entrypoint' strings or ``markdown.Extension`` objects. Defaults to ``['codehilite']``. .. versionchanged:: 0.7 Markdown 2.5 changed the syntax for passing extensions, and for configuring extensions. In particular, configuring an extension by passing keyword arguments along with the import string is now deprecated. Instead, these options need to be passed in a dict. For more information, see ``FLATPAGES_EXTENSION_CONFIGS``. ``FLATPAGES_EXTENSION_CONFIGS`` .. versionadded:: 0.7 Extension config dictionary for configuring extensions passed by their import string. For each extension, ``FLATPAGES_EXTENSION_CONFIGS`` contains a dict of configuration settings passed as strings. For example, to enable linenums in ``codehilite``: .. code-block:: python FLATPAGES_EXTENSION_CONFIGS = { 'codehilite': { 'linenums': 'True' } } `See the Markdown 3 documentation for more details `_ ``FLATPAGES_AUTO_RELOAD`` Whether to reload pages at each request. See :ref:`laziness-and-caching` for more details. The default is to reload in ``DEBUG`` mode only. ``FLATPAGES_LEGACY_META_PARSER`` .. versionadded:: 0.8 Controls whether to use the newer parser based on tokenising metadata with libyaml. Setting this to true reverts to the simpler method of parsing metadata used in versions <0.8, which requires the metadata block be terminated by a newline, or else if there's no metadata that a leading newline be present. Intended to provide a fallback in case of bugs with the newer parser. Please note that multiple FlatPages instances can be configured by using a name for the FlatPages instance at initializaton time: .. code-block:: python flatpages = FlatPages(name="blog") To configure this instance, you must use modified configuration keys, by adding the uppercase name to the configuration variable names: ``FLATPAGES_BLOG_*`` How it works ------------ When first needed (see :ref:`laziness-and-caching` for more about this), the extension loads all pages from the filesystem: a :class:`Page` object is created for all files in ``FLATPAGES_ROOT`` whose name ends with ``FLATPAGES_EXTENSION``. Each of these objects is associated to a path: the slash-separated (whatever the OS) name of the file it was loaded from, relative to the pages root, and excluding the extension. For example, for an app in ``C:\myapp`` with the default configuration, the path for the ``C:\myapp\pages\lorem\ipsum.html`` is ``lorem/ipsum``. Each file is made of a `YAML`_ mapping of metadata, and the page body:: title: Hello published: 2010-12-22 --- Hello, *World*! Lorem ipsum dolor sit amet, … The body format defaults to `Markdown`_ with `Pygments`_ baked in if available, but depends on the ``FLATPAGES_HTML_RENDERER`` configuration value. .. _YAML: http://www.yaml.org/ .. _Markdown: http://daringfireball.net/projects/markdown/ .. _Pygments: http://pygments.org/ To use Pygments, you need to include the style declarations separately. You can get them with :func:`.pygments_style_defs`:: @app.route('/pygments.css') def pygments_css(): return pygments_style_defs('tango'), 200, {'Content-Type': 'text/css'} and in templates: .. code-block:: html+jinja .. highlight:: YAML Delimiting YAML Metadata ~~~~~~~~~~~~~~~~~~~~~~~~ .. versionadded:: 0.8 .. note:: The ``FLATPAGES_LEGACY_META_PARSER`` flag can be used to re-enable the legacy metadata behaviour. In previous versions, YAML metadata was terminated by a newline. This meant it was impossible to use e.g. multi-line strings in the metadata, to import files from other markdown tools like Obsidian, or worse that if your page had no metadata at all it needed to start with an empty line. Starting with v0.8, YAML can now be delimited by wrapping it with ``---``:: --- title: Hello World author: J.L Coolman --- Hello, world! Or using YAML 'end document' specifiers like:: --- title: Hello Again author: J.L Coolman ... Hello, world! In all cases, the leading ``---`` is optional. With this change, it's now possible to have pages with no-metadata by starting them with:: --- --- Hello, this is my page Or:: --- ... Hello, this is a page too Or even just launching in to the body:: Hello, this is also a page! .. warning:: In previous versions, metadata was terminated with a new line. The updated metadata parser attempts to preserve backwards compatability as much as possible, but unexpected behaviour can occur if the page starts with text that looks ambiguously 'YAML-like'. For example:: hello: world This is my blog: We strongly advise using an explicit delimiter. In addition, if your metadata features multi-line blocks, you **must** specify an start and end delimiter, or else the parser may cut the metadata at the first blank line. .. highlight:: python Using custom Markdown extensions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. versionadded:: 0.4 By default, Flask-FlatPages renders flatpage body using `Markdown`_ with `Pygments`_ format. This means passing ``['codehilite']`` extensions list to ``markdown.markdown`` function. But sometimes you need to customize things, like using another extension or disable default approach, this possible by passing special config. For example, using another extension:: FLATPAGES_MARKDOWN_EXTENSIONS = ['codehilite', 'headerid'] Or disabling default approach:: FLATPAGES_MARKDOWN_EXTENSIONS = [] Using custom HTML renderers ~~~~~~~~~~~~~~~~~~~~~~~~~~~ As pointed above, by default Flask-FlatPages expects that flatpage body contains `Markdown`_ markup, so uses ``markdown.markdown`` function to render its content. But due to ``FLATPAGES_HTML_RENDERER`` setting you can specify different approach for rendering flatpage body. The most common necessity of using custom HTML renderer is modifyings default Markdown approach (e.g. by pre-rendering Markdown flatpages with Jinja), or using different markup for rendering flatpage body (e.g. ReStructuredText). Examples below introduce how to use custom renderers for those needs. Pre-rendering Markdown flatpages with Jinja ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :: from flask import Flask, render_template_string from flask_flatpages import FlatPages from flask_flatpages.utils import pygmented_markdown def my_renderer(text): prerendered_body = render_template_string(text) return pygmented_markdown(prerendered_body) # Or, if you wish to render using the markdown extensions # listed in FLATPAGES_MARKDOWN_EXTENSIONS: #return pygmented_markdown(prerendered_body, flatpages=pages) app = Flask(__name__) app.config['FLATPAGES_HTML_RENDERER'] = my_renderer pages = FlatPages(app) ReStructuredText flatpages ^^^^^^^^^^^^^^^^^^^^^^^^^^ .. note:: For rendering ReStructuredText you need to add `docutils `_ to your project requirements. :: from docuitls.core import publish_parts from flask import Flask from flask_flatpages import FlatPages def rst_renderer(text): parts = publish_parts(source=text, writer_name='html') return parts['fragment'] app = Flask(__name__) app.config['FLATPAGES_HTML_RENDERER'] = rst_renderer pages = FlatPages(app) .. _laziness-and-caching: Laziness and caching -------------------- :class:`.FlatPages` does not hit the filesystem until needed but when it does, it reads all pages from the disk at once. Then, pages are not loaded again unless you explicitly ask for it with :meth:`.FlatPages.reload`, or on new requests depending on the configuration. (See ``FLATPAGES_AUTO_RELOAD``.) This design was decided with `Frozen-Flask`_ in mind but should work even if you don’t use it: you already restart your production server on code changes, you just have to do it on page content change too. This can make sense if the pages are deployed alongside the code in version control. .. _Frozen-Flask: http://packages.python.org/Frozen-Flask/ If you have many pages and loading takes a long time, you can force it at initialization time so that it’s done by the time the first request is served:: pages = FlatPages(app) pages.get('foo') # Force loading now. foo.html may not even exist. Loading everything every time may seem wasteful, but the impact is mitigated by caching: if a file’s modification time hasn’t changed, it is not read again and the previous :class:`.Page` object is re-used. Likewise, the YAML and Markdown parsing is both lazy and cached: not done until needed, and not done again if the file did not change. API --- .. module:: flask_flatpages .. autoclass:: FlatPages :members: init_app, get, get_or_404, __iter__, reload Example usage:: pages = FlatPages(app) @app.route('/') def index(): # Articles are pages with a publication date articles = (p for p in pages if 'published' in p.meta) # Show the 10 most recent articles, most recent first. latest = sorted(articles, reverse=True, key=lambda p: p.meta['published']) return render_template('articles.html', articles=latest[:10]) @app.route('//') def page(path): page = pages.get_or_404(path) template = page.meta.get('template', 'flatpage.html') return render_template(template, page=page) .. autoclass:: Page() :members: With the ``hello.html`` page defined earlier .. code-block:: YAML # hello.html title: Hello published: 2010-12-22 Hello, *World*! Lorem ipsum dolor sit amet, … :: >>> page = pages.get('hello') >>> page.meta # PyYAML converts YYYY-MM-DD to a date object {'title': 'Hello', 'published': datetime.date(2010, 12, 22)} >>> page['title'] 'Hello' >>> page.body 'Hello, *World*!\n\nLorem ipsum dolor sit amet, \u2026' >>> page.html '

Hello, World!

\n

Lorem ipsum dolor sit amet, \u2026

' .. automethod:: __getitem__ .. automethod:: __html__ .. autofunction:: pygmented_markdown .. autofunction:: pygments_style_defs Changelog --------- .. release-notes:: v0.7.2 ~~~~~~~~~~~~~ Release Date: 2020-04-19 Bug Fixes ^^^^^^^^^ - Fixed a bug arising when the user overrides the default markdown extensions, but does not use Pygments. Apologies to anyone who didn't want codehiliting! (`#73 `_) Documentation Changes ^^^^^^^^^^^^^^^^^^^^^ - Update documentation to use the latest version of the Flask template. - Add towncrier config for auto-generating release notes Other Notes ^^^^^^^^^^^ - This project currently supports Python 3.8 and newer. v0.7.1 ~~~~~~~~~~~~~ * Small bump to dependency versions to resolve security alerts. v0.7.0 ~~~~~~~~~~~~~ * Update to Markdown 3.0 with new extension loading syntax. * Added `FLATPAGES_EXTENSION_CONFIGS` for configuring extensions specified by import string. * Add support for loading pages from Flask instance folder * Add a case insensitive loading option v0.6.1 ~~~~~~~~~~~~~ * Update dependencies to `Flask>=1.0` (as Flask 0.12.1 has known vulnerabilities). * Pin `Markdown<=3.0` as the Markdown extension API has changed. v0.6 ~~~~~~~~~~~ Release Date: 2015-02-09 * Python 3 support. * Allow multiple file extensions for FlatPages. * The renderer function now optionally takes a third argument, namely the :class:`Page` instance. * It is now possible to instantiate multiple instances of :class:`FlatPages` with different configurations. This is done by specifying an additional parameter ``name`` to the initializer and adding the same name in uppercase to the respective Flask configuration settings. v0.5 ~~~~~~~~~~~ Release Date: 2013-04-02 * Change behavior of passing ``FLATPAGES_MARKDOWN_EXTENSIONS`` to renderer function, now the :class:`FlatPages` instance is optionally passed as second argument. This allows more robust renderer functions. v0.4 ~~~~~~~~~~~ Release Date: 2013-04-01 * Add ``FLATPAGES_MARKDOWN_EXTENSIONS`` config to setup list of Markdown extensions to use with default HTML renderer. * Fix a bug with non-ASCII filenames. v0.3 ~~~~~~~~~~~ Release Date: 2012-07-03 * Add :meth:`.FlatPages.init_app` * Do not use namespace packages anymore: rename the package from ``flaskext.flatpages`` to ``flask_flatpages`` * Add configuration files for testing with tox and Travis. v0.2 ~~~~~~~~~~~ Release Date: 2011-06-02 Bugfix and cosmetic release. Tests are now installed alongside the code. v0.1 ~~~~~~~~~~~ Release Date: 2011-02-06. First public release. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/docs/requirements.txt0000644000175100017510000000007115125324470020531 0ustar00runnerrunnerPallets-Sphinx-Themes Pygments>=2.2.0 Sphinx>=1.8.1 reno ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1767221582.0136006 flask_flatpages-0.9.0/flask_flatpages/0000755000175100017510000000000015125324516017446 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/flask_flatpages/__init__.py0000644000175100017510000000114615125324470021560 0ustar00runnerrunner""" Flask-FlatPages provides a collection of pages to your Flask application. Pages are built from "flat" text files as opposed to a relational database. :copyright: (c) 2010-2015 by Simon Sapin, 2013-2015 by Igor Davydenko. :license: BSD, see LICENSE for more details. """ from .flatpages import FlatPages from .page import Page from .utils import pygmented_markdown, pygments_style_defs from . import parsers __author__ = "Simon Sapin, Igor Davydenko, Padraic Calpin" __license__ = "BSD License" __version__ = "0.9.0" __all__ = ["FlatPages", "Page", "parsers", "pygmented_markdown", "pygments_style_defs"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/flask_flatpages/flatpages.py0000755000175100017510000003024515125324470021774 0ustar00runnerrunner"""Flatpages extension.""" from __future__ import annotations import os import warnings from collections.abc import Iterator from functools import cached_property from inspect import getfullargspec from flask import abort, current_app, Flask from werkzeug.utils import import_string from .page import Page from .parsers import legacy_parser, libyaml_parser from .utils import ( WrappedRenderer, force_unicode, pygmented_markdown, ) class FlatPages: """A collection of :class:`Page` objects.""" #: Default configuration for FlatPages extension default_config = ( ("root", "pages"), ("extension", ".html"), ("encoding", "utf-8"), ("html_renderer", pygmented_markdown), ("markdown_extensions", ["codehilite"]), ("extension_configs", {}), ("auto_reload", "if debug"), ("case_insensitive", False), ("instance_relative", False), ("legacy_meta_parser", False), ) def __init__(self, app: Flask | None = None, name: str | None = None): """Initialize FlatPages extension. :param app: Your application. Can be omitted if you call :meth:`init_app` later. :type app: A :class:`~flask.Flask` instance :param name: The name for this FlatPages instance. Used for looking up config values using 'FLATPAGES_%s_%s' % (name.upper(), key) By default, no name is used, so configuration is done by specifying config values using 'FLATPAGES_%s' % (key) Typically, you only need to set this parameter if you want to use multiple :class:`FlatPages instances within the same Flask application. :type name: string .. versionchanged:: 0.6 New parameter `name` to support multiple FlatPages instances. """ self.name = name if name is None: self.config_prefix = "FLATPAGES" else: self.config_prefix = "_".join(("FLATPAGES", name.upper())) #: dict of filename: (page object, mtime when loaded) self._file_cache = {} self._root = None if app: self.init_app(app) def __iter__(self) -> Iterator[Page]: """Iterate on all :class:`Page` objects.""" return iter(self._pages.values()) def config(self, key: str): """Read actual configuration from Flask application config. :param key: Lowercase config key from :attr:`default_config` tuple """ return current_app.config["_".join((self.config_prefix, key.upper()))] def get(self, path: str, default: Page | None = None) -> Page: """ Return the :class:`Page` object at ``path``. Returns ``default`` if there is no such page. """ # This may trigger the property. Do it outside of the try block. pages = self._pages try: return pages[path] except KeyError: return default def get_or_404(self, path: str) -> Page: """ Return the :class:`Page` object at ``path``. Raise Flask's 404 error if there is no such page. """ page = self.get(path) if not page: abort(404) return page def init_app(self, app: Flask): """ Use to initialize an application. Ueful for passing an app later and app factory patterns. :param app: your application :type app: a :class:`~flask.Flask` instance """ # Store default config to application for key, value in self.default_config: config_key = "_".join((self.config_prefix, key.upper())) app.config.setdefault(config_key, value) # Register function to forget all pages if necessary app.before_request(self._conditional_auto_reset) # And finally store application to current instance and current # instance to application if "flatpages" not in app.extensions: app.extensions["flatpages"] = {} app.extensions["flatpages"][self.name] = self self._app = app @property def app(self) -> Flask: """ The Flask Application associated with this extension. Accessing the application instance this way is not advised. This property exists to ensure backwards compatability, and in future will wrap `flask.current_app`, the recommended way to access the app instance. """ warnings.warn( "Flask recommends accessing the app instance via " "`flask.current_app`. In future releases, this will wrap " "current_app, and raise a RuntimeError if there is no " "application context present.", DeprecationWarning, ) return self._app @app.setter def app(self, app_instance: Flask): warnings.warn( "This attribute should be managed by self.init_app instead. " "This will raise an AttributeError from version 1.0 onwards.", DeprecationWarning, ) self._app = app_instance def reload(self): """Forget all pages. All pages will be reloaded next time they're accessed. """ try: # This will "unshadow" the cached_property. # The property will be re-executed on next access. del self.__dict__["_pages"] except KeyError: pass @property def root(self) -> str: """Full path to the directory where pages are looked for. This corresponds to the `FLATPAGES_%(name)s_ROOT` config value, interpreted as relative to the app's root directory, or as relative to the app's instance folder if `FLATPAGES_%(name)s_INSTANCE_RELATIVE` is set to `True`. """ if self.config("instance_relative"): root_dir = os.path.join( current_app.instance_path, self.config("root") ) else: root_dir = os.path.join(current_app.root_path, self.config("root")) return force_unicode(root_dir) def _conditional_auto_reset(self): """Reset if configured to do so on new requests.""" auto = self.config("auto_reload") if auto == "if debug": auto = self.app.debug if auto: self.reload() def _load_file(self, path: str, filename: str, rel_path: str): """ Load file from file system and cache it. We store the result as a tuple of :class:`Path` and the file `mtime`. """ mtime = os.path.getmtime(filename) cached = self._file_cache.get(filename) if cached and cached[1] == mtime: page = cached[0] else: encoding = self.config("encoding") with open(filename, encoding=encoding) as handler: content = handler.read() page = self._parse(content, path, rel_path) self._file_cache[filename] = (page, mtime) return page @cached_property def _pages(self) -> dict[str, Page]: """ Walk the page root directory and return a dict of pages. Returns a dictionary of pages keyed by their path. """ def _walker(): """ Walk over directory and find all possible flatpages. Returns files which end with the string or sequence given by ``FLATPAGES_%(name)s_EXTENSION``. """ for cur_path, _, filenames in os.walk(self.root): rel_path = cur_path.replace(self.root, "").lstrip(os.sep) path_prefix = tuple(rel_path.split(os.sep)) if rel_path else () for name in filenames: if not name.endswith(extension): continue full_name = os.path.join(cur_path, name) name_without_extension = [ name[: -len(item)] for item in extension if name.endswith(item) ][0] path = "/".join(path_prefix + (name_without_extension,)) if self.config("case_insensitive"): path = path.lower() yield (path, full_name, rel_path) # Read extension from config extension = self.config("extension") # Support for multiple extensions if isinstance(extension, str): if "," in extension: extension = tuple(extension.split(",")) else: extension = (extension,) elif isinstance(extension, (list, set)): extension = tuple(extension) # FlatPage extension should be a string or a sequence if not isinstance(extension, tuple): raise ValueError( "Invalid value for FlatPages extension. Should be a string or " "a sequence, got {0} instead: {1}".format( type(extension).__name__, extension ) ) pages = {} for path, full_name, rel_path in _walker(): if path in pages: raise ValueError( "Multiple pages found which correspond to the same path. " "This error can arise when using multiple extensions." ) pages[path] = self._load_file(path, full_name, rel_path) return pages def _parse(self, content, path, rel_path): """Parse a flatpage file, i.e. read and parse its meta data and body. :return: initialized :class:`Page` instance. """ if self.config("legacy_meta_parser"): meta, content = legacy_parser(content) else: meta, content = libyaml_parser(content, path) # Now we ready to get HTML renderer function html_renderer = self.config("html_renderer") # If function is not callable yet, import it if not callable(html_renderer): html_renderer = import_string(html_renderer) # Make able to pass custom arguments to renderer function html_renderer = self._smart_html_renderer(html_renderer) # Assign the relative path (to root) for use in the page object folder = rel_path # Initialize and return Page instance return Page(path, meta, content, html_renderer, folder) def _smart_html_renderer(self, html_renderer) -> WrappedRenderer[Page]: """ Wrappper to enable rendering functions with differing signatures. We stay backwards compatible by using reflection, i.e. we inspect the given rendering function's signature in order to find out how many arguments the function takes. .. versionchanged:: 0.6 Support for HTML renderer functions with signature ``f(body, flatpages, page)``, where ``page`` is an instance of :class:`Page`. .. versionchanged:: 0.5 Support for HTML renderer functions with signature ``f(body, flatpages)``, where ``flatpages`` is an instance of :class:`FlatPages`. """ def wrapper(page: Page) -> str: """Wrap HTML renderer function. Pass arguments to the renderer based on the number of arguments. * 1 argument -> page body * 2 arguments -> page body, flatpages instance * 3 arguments -> page body, flatpages instance, page instance """ body = page.body try: args_length = len(getfullargspec(html_renderer).args) except TypeError: return html_renderer(body) if args_length == 1: return html_renderer(body) elif args_length == 2: return html_renderer(body, self) elif args_length == 3: return html_renderer(body, self, page) raise ValueError( "HTML renderer function {0!r} not supported by " "Flask-FlatPages, wrong number of arguments: {1}.".format( html_renderer, args_length ) ) return wrapper ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/flask_flatpages/imports.py0000644000175100017510000000024115125324470021511 0ustar00runnerrunner"""Conditional imports.""" try: from pygments.formatters import HtmlFormatter as PygmentsHtmlFormatter except ImportError: PygmentsHtmlFormatter = None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/flask_flatpages/page.py0000644000175100017510000000514515125324470020740 0ustar00runnerrunner"""Define flatpage instance.""" from __future__ import annotations from functools import cached_property from io import StringIO from typing import Any import yaml from .utils import WrappedRenderer class Page: """Simple class to store all necessary information about a flatpage. Main purpose is to render the page's content with a ``html_renderer`` function. """ def __init__( self, path: str, meta: str, body: str, html_renderer: WrappedRenderer["Page"], folder: str, ): """Initialize Page instance. :param path: Page path. :param meta: Page meta data in YAML format. :param body: Page body. :param html_renderer: HTML renderer function. :param folder: The folder the page is contained in. """ #: Path this page was obtained from, as in ``pages.get(path)`` self.path = path #: Content of the page self._meta = meta self.body = body #: Renderer function self.html_renderer = html_renderer #: The name of the folder the page is contained in. self.folder = folder def __getitem__(self, name: str): """Shortcut for accessing metadata. ``page['title']`` or, in a template, ``{{ page.title }}`` are equivalent to ``page.meta['title']``. """ return self.meta[name] def __html__(self) -> str: """ Return HTML for use in Jinja templates. In a template, ``{{ page }}`` is equivalent to ``{{ page.html|safe }}``. """ return self.html def __repr__(self) -> str: """Machine representation of :class:`Page` instance.""" return f"" @cached_property def html(self) -> str: """Content of the page, rendered as HTML by the configured renderer.""" return self.html_renderer(self) @cached_property def meta(self) -> dict[str, Any]: """Store a dict of metadata parsed from the YAML header of the file.""" meta = {} for doc in yaml.safe_load_all(StringIO(self._meta)): if doc is not None: meta.update(doc) # YAML documents can be any type but we want a dict # eg. yaml.safe_load('') -> None # yaml.safe_load('- 1\n- a') -> [1, 'a'] if not meta: return {} if not isinstance(meta, dict): raise ValueError( "Expected a dict in metadata for '{0}', got {1}".format( self.path, type(meta).__name__ ) ) return meta ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1767221582.0136006 flask_flatpages-0.9.0/flask_flatpages/parsers/0000755000175100017510000000000015125324516021125 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/flask_flatpages/parsers/__init__.py0000644000175100017510000000021315125324470023231 0ustar00runnerrunner"""Python package for metadata parsers.""" from .yaml import legacy_parser, libyaml_parser __all__ = ["legacy_parser", "libyaml_parser"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/flask_flatpages/parsers/yaml.py0000644000175100017510000000467415125324470022453 0ustar00runnerrunner"""Module for YAML metadata parser.""" import operator from itertools import takewhile from yaml import ( BlockMappingStartToken, BlockSequenceStartToken, DocumentEndToken, DocumentStartToken, FlowMappingStartToken, FlowSequenceStartToken, KeyToken, SafeLoader, ScalarToken, ) from ..utils import NamedStringIO START_TOKENS = ( BlockMappingStartToken, BlockSequenceStartToken, DocumentStartToken, FlowMappingStartToken, FlowSequenceStartToken, KeyToken, ) def _check_continue_parsing_tokens(token): return not ( isinstance(token, (DocumentStartToken, DocumentEndToken)) or token is None ) def _check_newline_token(token): return ( isinstance(token, ScalarToken) and token.style is None and "\n" in token.value ) def legacy_parser(content): """Legacy YAML parser.""" lines = iter(content.split("\n")) # Read lines until an empty line is encountered. meta = "\n".join(takewhile(operator.methodcaller("strip"), lines)) # The rest is the content. `lines` is an iterator so it continues # where `itertools.takewhile` left it. content = "\n".join(lines) return meta, content def libyaml_parser(content, path): """YAML parser.""" yaml_loader = SafeLoader(NamedStringIO(content, path)) yaml_loader.get_token() # Get stream start token token = yaml_loader.get_token() if not isinstance(token, START_TOKENS): meta = "" content = content.lstrip("\n") else: lines = content.split("\n") if isinstance(token, DocumentStartToken): token = yaml_loader.get_token() newline_token = None while _check_continue_parsing_tokens(token): try: token = yaml_loader.get_token() if _check_newline_token(token) and newline_token is None: newline_token = token except Exception: break if token is None and newline_token is None: meta = content content = "" else: if token is not None: meta_end_line = token.end_mark.line + 1 else: meta_end_line = newline_token.start_mark.line meta_end_line += lines[meta_end_line:].index("") meta = "\n".join(lines[:meta_end_line]) content = "\n".join(lines[meta_end_line:]).lstrip("\n") return meta, content ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/flask_flatpages/utils.py0000644000175100017510000000611715125324470021164 0ustar00runnerrunner"""Utility functions to render Markdown text to HTML.""" from typing import Callable, TypeVar import markdown from markdown.extensions import codehilite from .imports import PygmentsHtmlFormatter from io import StringIO P = TypeVar("P") WrappedRenderer = Callable[[P], str] class NamedStringIO(StringIO, object): """Subclass adding a Name to :class:`StringIO` objects.""" def __init__(self, content, name): """ Initialise the NamedStringIO. :param content: The string to be treated as a stream :param name: The name to attach to the stream. Will be consumed in e.g. ReaderErrors raised by pyyaml. """ super().__init__(content) self.name = name def force_unicode(value, encoding="utf-8", errors="strict"): """Convert bytes or any other Python instance to string.""" if isinstance(value, str): return value return value.decode(encoding, errors) def pygmented_markdown(text: str, flatpages=None): """Render Markdown text to HTML. Uses the `CodeHilite`_ extension only if `Pygments`_ is available. If `Pygments`_ is not available, "codehilite" is removed from list of extensions. If you need other extensions, set them up using the ``FLATPAGES_MARKDOWN_EXTENSIONS`` setting, which should be a sequence of strings or Markdown Extension objects. Extensions specified with entrypoint strings should be configured using ``FLATPAGES_EXTENSION_CONFIGS``. .. _CodeHilite: http://www.freewisdom.org/projects/python-markdown/CodeHilite .. _Pygments: http://pygments.org/ """ if flatpages: extensions = flatpages.config("markdown_extensions") extension_configs = flatpages.config("extension_configs") else: extensions = [] extension_configs = {} if PygmentsHtmlFormatter is None: original_extensions = extensions original_config = extension_configs extensions = [] extension_configs = {} for extension in original_extensions: if isinstance(extension, str) and "codehilite" in extension: continue elif isinstance(extension, codehilite.CodeHiliteExtension): continue extensions.append(extension) if isinstance(extension, str): if extension in original_config: extension_configs[extension] = original_config[extension] elif not extensions: extensions = ["codehilite"] return markdown.markdown( text, extensions=extensions, extension_configs=extension_configs ) def pygments_style_defs(style: str = "default") -> str: """:return: the CSS definitions for the `CodeHilite`_ Markdown plugin. :param style: The Pygments `style`_ to use. Only available if `Pygments`_ is. .. _CodeHilite: http://www.freewisdom.org/projects/python-markdown/CodeHilite .. _Pygments: http://pygments.org/ .. _style: http://pygments.org/docs/styles/ """ formatter = PygmentsHtmlFormatter(style=style) return formatter.get_style_defs(".codehilite") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/pyproject.toml0000644000175100017510000000744415125324470017244 0ustar00runnerrunner[build-system] requires = [ "setuptools", "wheel", ] build-backend = "setuptools.build_meta" [project] name = "Flask-FlatPages" dynamic = ["version"] authors = [ { name = "Simon Sapin" }, { name = "Igor Davydenko" }, { name = "Padraic Calpin" }, ] maintainers = [{ name = "Padraic Calpin", email = "itsme@padraic.xyz" }] description = "Provides flat static pages to a Flask application" readme = "README.rst" classifiers = [ "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] requires-python = ">=3.8" dependencies = [ "Flask > 1.0", "Jinja2 >= 2.10.2", "Markdown >= 2.5", "PyYAML > 5.3.1", ] [project.urls] Repository = "https://github.com/Flask-FlatPages/Flask-FlatPages" Documentation = "https://flask-flatpages.readthedocs.io/en/latest/" [project.optional-dependencies] codehilite = ["Pygments>=2.5.2"] [dependency-groups] test = [ "pytest", "pytest-cov", "pytest-mock>=3.14.1", "reno", "ruff", "typing-extensions; python_version < '3.10'", ] codehilite = ["Pygments"] [tool.uv] default-groups = "all" [tool.setuptools.package-dir] flask_flatpages = "flask_flatpages" [tool.setuptools.dynamic] version = { attr = "flask_flatpages.__version__" } [tool.tox] envlist = [ "docs", "lint", "fmt", "py38", "py38-pygments", "py39", "py39-pygments", "py310", "py310-pygments", "py311", "py311-pygments", "py312", "py312-pygments", "py313", "py313-pygments", "py314", "py314-pygments", ] [tool.tox.env_run_base] dependency_groups = ["test"] commands = [ [ "python", "-m", "pytest", "-v", "--cov=flask_flatpages", "--cov-branch", "--cov-report", "term", { replace = "posargs", extend = true } ], ] [tool.tox.env.py38-pygments] extras = ["codehilite"] [tool.tox.env.py39-pygments] extras = ["codehilite"] [tool.tox.env.py310-pygments] extras = ["codehilite"] [tool.tox.env.py311-pygments] extras = ["codehilite"] [tool.tox.env.py312-pygments] extras = ["codehilite"] [tool.tox.env.py313-pygments] extras = ["codehilite"] [tool.tox.env.py314-pygments] extras = ["codehilite"] [tool.tox.env.docs] commands = [ [ "sphinx-build", "-d", "{toxworkdir}/docs/_build/_doctree", "docs", "docs/_build/html", "--color", "-W", "-b", "html", ], ] deps = ["-r docs/requirements.txt"] [tool.tox.env.lint] skip_install = true deps = ["ruff"] commands = [ ["ruff", "check", "flask_flatpages/"], ["ruff", "format", "--check", "flask_flatpages/"], ] [tool.tox.env.fmt] skip_install = true deps = ["ruff"] commands = [["ruff", "format", {replace = "posargs", default="flask_flatpages", extend=true}]] [tool.tox.env.reno] skip_install = true deps = ["reno"] commands = [["reno", { replace = "posargs", extend = true }]] [tool.flake8] application-import-names = "flask_flatpages" ignore = ["W503"] import-order-style = "smarkets" max-complexity = 20 [tool.ruff] line-length = 79 target-version = "py310" [tool.ruff.format] quote-style = "double" exclude = ["docs/conf.py"] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1767221582.0146005 flask_flatpages-0.9.0/setup.cfg0000644000175100017510000000004615125324516016141 0ustar00runnerrunner[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/setup.py0000644000175100017510000000010515125324470016025 0ustar00runnerrunnerimport setuptools if __name__ == "__main__": setuptools.setup() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1767221582.0136006 flask_flatpages-0.9.0/tests/0000755000175100017510000000000015125324516015462 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/tests/test_flask_flatpages.py0000644000175100017510000004122415125324470022223 0ustar00runnerrunner# coding: utf8 """ Tests for Flask-FlatPages ~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2010 by Simon Sapin. :license: BSD, see LICENSE for more details. """ from __future__ import annotations import datetime import operator import os import shutil import sys import unicodedata import flask import yaml import pytest from flask import Flask from flask_flatpages import FlatPages, pygments_style_defs from flask_flatpages.imports import PygmentsHtmlFormatter from werkzeug.exceptions import NotFound utc = datetime.timezone.utc from unittest.mock import patch @pytest.fixture def default_paths(): return set( [ "codehilite", "extra", "foo", "foo/bar", "foo/lorem/ipsum", "headerid", "hello", "meta_styles/closing_block_only", "meta_styles/yaml_style", "meta_styles/jekyll_style", "meta_styles/multi_line", "meta_styles/no_meta", "toc", ] ) @pytest.fixture def all_paths(default_paths): return default_paths | {"foo/42/not_a_page", "not_a_page"} @pytest.fixture def paths_excluding(all_paths): def filtered_paths(*args): paths = all_paths for arg in args: paths = paths.remove(arg) return paths return filtered_paths def _check_auto_reset(pages: FlatPages, should_reset: bool): bar = pages.get("foo/bar") assert bar.body == "" filename = os.path.join(pages.root, "foo", "bar.html") with open(filename, "w") as fd: fd.write("\nrewritten") # simulate a request (before_request functions are called) # pages.reload() is not call explicitly with flask.current_app.test_request_context(): flask.current_app.preprocess_request() bar2 = pages.get("foo/bar") if should_reset: assert bar2.body == "rewritten" assert bar2 is not bar else: assert bar2.body == "" assert bar2 is bar def test_caching(app_with_context, temp_pages): pages = temp_pages(app_with_context) foo = pages.get("foo") bar = pages.get("foo/bar") filename = os.path.join(pages.root, "foo", "bar.html") with open(filename, "w") as fd: fd.write("\nrewritten") pages.reload() foo2 = pages.get("foo") bar2 = pages.get("foo/bar") # Page objects are cached and unmodified files (according to the # modification date) are not parsed again. assert foo2 is foo assert bar2 is not bar assert bar2.body != bar.body def test_configured_auto_reset(app_with_context, temp_pages): app_with_context.config["FLATPAGES_AUTO_RELOAD"] = True pages = temp_pages(app_with_context) _check_auto_reset(pages, True) def test_configured_no_auto_reset(app_with_context, temp_pages): app_with_context.debug = True app_with_context.config["FLATPAGES_AUTO_RELOAD"] = False pages = temp_pages(app_with_context) _check_auto_reset(pages, should_reset=False) def test_consistency(app_with_context, flatpages_factory): pages = flatpages_factory(app_with_context) for page in pages: assert pages.get(page.path) is page def test_debug_auto_reset(app_with_context, temp_pages): app_with_context.debug = True pages = temp_pages(app_with_context) _check_auto_reset(pages, should_reset=True) def test_default_no_auto_reset(app_with_context, temp_pages): pages = temp_pages(app_with_context) _check_auto_reset(pages, should_reset=False) @pytest.fixture def pages_with_extension(app_with_context, flatpages_factory): def _initialised_pages(extensions): app_with_context.config["FLATPAGES_EXTENSION"] = extensions flatpages = flatpages_factory(app_with_context) return set(page.path for page in flatpages) yield _initialised_pages def test_extension_sequence(all_paths, pages_with_extension): assert all_paths == pages_with_extension([".html", ".txt"]) def test_extension_comma(all_paths, pages_with_extension): assert all_paths == pages_with_extension(".html,.txt") def test_extension_set(all_paths, pages_with_extension): assert all_paths == pages_with_extension(set([".html", ".txt"])) def test_extension_tuple(all_paths, pages_with_extension): assert all_paths == pages_with_extension((".html", ".txt")) def test_catch_conflicting_paths(app_with_context, temp_pages): app_with_context.config["FLATPAGES_EXTENSION"] = [".html", ".txt"] pages = temp_pages(app_with_context) original_file = os.path.join(pages.root, "hello.html") target_file = os.path.join(pages.root, "hello.txt") shutil.copyfile(original_file, target_file) pages.reload() with pytest.raises(ValueError): pages.get("hello") def test_case_insensitive(app_with_context, temp_pages, all_paths): app_with_context.config["FLATPAGES_EXTENSION"] = [".html", ".txt"] app_with_context.config["FLATPAGES_CASE_INSENSITIVE"] = True pages = temp_pages(app_with_context) original_file = os.path.join(pages.root, "hello.html") upper_file = os.path.join(pages.root, "Hello.html") txt_file = os.path.join(pages.root, "hello.txt") shutil.move(original_file, upper_file) pages.reload() assert all_paths == set(p.path for p in pages) shutil.copyfile(upper_file, txt_file) pages.reload() with pytest.raises(ValueError): pages.get("hello") def test_get(app_with_context, flatpages_factory): pages = flatpages_factory(app_with_context) assert pages.get("foo/bar").path == "foo/bar" assert pages.get("nonexistent") == None assert pages.get("nonexistent", 42) == 42 def test_get_or_404(app_with_context, flatpages_factory): pages = flatpages_factory(app_with_context) assert pages.get_or_404("foo/bar").path == "foo/bar" with pytest.raises(NotFound): pages.get_or_404("nonexistant") def test_iter(app_with_context, flatpages_factory, default_paths): pages = flatpages_factory(app_with_context) assert set(p.path for p in pages) == default_paths def test_lazy_loading(app_with_context, temp_pages): pages = temp_pages(app_with_context) bar = pages.get("foo/bar") # bar.html is normally empty assert bar.meta == {} assert bar.body == "" filename = os.path.join(pages.root, "foo", "bar.html") # write as pages is already constructed with open(filename, "a") as fd: fd.write("a: b\n\nc") bar = pages.get("foo/bar") assert bar.meta == {} assert bar.body == "" pages.reload() bar = pages.get("foo/bar") # bar was just loaded from the disk assert bar.meta == {"a": "b"} assert bar.body == "c" def test_markdown(app_with_context, flatpages_factory): pages = flatpages_factory(app_with_context) foo = pages.get("foo") assert foo.body == "Foo *bar*\n" assert foo.html == "

Foo bar

" def test_instance_relative(tmpdir): source = os.path.join(os.path.dirname(__file__), "pages") dest = os.path.join(tmpdir, "instance", "pages") shutil.copytree(source, dest) app = Flask(__name__, instance_path=os.path.join(tmpdir, "instance")) app.config["FLATPAGES_INSTANCE_RELATIVE"] = True pages = FlatPages(app) with app.app_context(): bar = pages.get("foo/bar") assert bar is not None def test_multiple_instance(app_with_context, temp_pages): """ This does a very basic test to see if two instances of FlatPages, one default instance and one with a name, do pick up the different config settings. """ app_with_context.debug = True app_with_context.config["FLATPAGES_DUMMY"] = True app_with_context.config["FLATPAGES_FUBAR_DUMMY"] = False p1 = temp_pages(app_with_context) assert p1.config("DUMMY") == app_with_context.config["FLATPAGES_DUMMY"] p2 = temp_pages(app_with_context, "fubar") assert ( p2.config("DUMMY") == app_with_context.config["FLATPAGES_FUBAR_DUMMY"] ) def _assert_unicode(pages: FlatPages): hello = pages.get("hello") assert hello.meta == {"title": "世界", "template": "article.html"} assert hello["title"] == "世界" assert hello.body == "Hello, *世界*!\n" # Markdown assert hello.html == "

Hello, 世界!

" def test_legacy_parser( app_with_context, flatpages_factory, default_paths, mocker ): app_with_context.config["FLATPAGES_LEGACY_META_PARSER"] = True yaml_mock = mocker.Mock(side_effect=ValueError("CannotHappen")) legacy_mock = mocker.Mock(return_value=(dict(), "Foo")) mocker.patch( "flask_flatpages.flatpages.legacy_parser", legacy_mock ) mocker.patch( "flask_flatpages.flatpages.libyaml_parser", yaml_mock ) pages = flatpages_factory(app_with_context) assert {page.path for page in pages} == default_paths yaml_mock.assert_not_called() assert legacy_mock.call_count == len(list(pages)) def test_other_encoding(app_with_context, flatpages_factory): pages = flatpages_factory(app_with_context) app_with_context.config["FLATPAGES_ENCODING"] = "shift_jis" app_with_context.config["FLATPAGES_ROOT"] = "pages_shift_jis" _assert_unicode(pages) def test_other_extension( app_with_context, flatpages_factory, all_paths, default_paths ): pages = flatpages_factory(app_with_context) txt_paths = all_paths - default_paths app_with_context.config["FLATPAGES_EXTENSION"] = ".txt" found_paths = {page.path for page in pages} assert found_paths == txt_paths assert found_paths.isdisjoint(default_paths) def test_other_html_renderer(): def body_renderer(body): return body.upper() def page_renderer(body, pages, page): return page.body.upper() def pages_renderer(body, pages): return pages.get("hello").body.upper() renderers = filter( None, ( operator.methodcaller("upper"), None, body_renderer, page_renderer, pages_renderer, ), ) for renderer in renderers: app = Flask(__name__) pages = FlatPages(app) with app.app_context(): app.config["FLATPAGES_HTML_RENDERER"] = renderer hello = pages.get("hello") assert hello.body == "Hello, *世界*!\n" # Upper-case, markdown not interpreted assert hello.html == "HELLO, *世界*!\n" @pytest.mark.skipif( PygmentsHtmlFormatter is None, reason="Pygments not installed" ) def test_pygments_style_defs(): styles = pygments_style_defs() assert ".codehilite" in styles def test_reloading(app_with_context, temp_pages): pages = temp_pages(app_with_context) bar = pages.get("foo/bar") # bar.html is normally empty assert bar.meta == {} assert bar.body == "" filename = os.path.join(pages.root, "foo", "bar.html") # rewrite already loaded page with open(filename, "w") as fd: # The newline is a separator between the (empty) metadata # and the source 'first' fd.write("\nfirst rewrite") bar2 = pages.get("foo/bar") # the disk is not hit again until requested assert bar2.meta == {} assert bar2.body == "" assert bar2 is bar # request reloading pages.reload() # write again with open(filename, "w") as fd: fd.write("\nsecond rewrite") # get another page pages.get("hello") # write again with open(filename, "w") as fd: fd.write("\nthird rewrite") # All pages are read at once when any is used bar3 = pages.get("foo/bar") assert bar3.meta == {} assert bar3.body == "second rewrite" # not third # Page objects are not reused when a file is re-read. assert bar3 is not bar2 # Removing does not trigger reloading either os.remove(filename) bar4 = pages.get("foo/bar") assert bar4.meta == {} assert bar4.body == "second rewrite" assert bar4 is bar3 pages.reload() bar5 = pages.get("foo/bar") assert bar5 is None # Reloading twice does not trigger an exception pages.reload() pages.reload() def test_unicode(app_with_context, flatpages_factory): pages = flatpages_factory(app_with_context) _assert_unicode(pages) def test_unicode_filenames(app_with_context, temp_pages, default_paths): def safe_unicode(sequence): if sys.platform != "darwin": return sequence return ''.join(unicodedata.normalize("NFC", item) for item in sequence) pages = temp_pages(app_with_context) assert {page.path for page in pages} == default_paths os.remove(os.path.join(pages.root, "foo", "lorem", "ipsum.html")) open(os.path.join(pages.root, "Unïcôdé.html"), "w").close() pages.reload() assert set(safe_unicode(page.path) for page in pages) == ( (default_paths | {"Unïcôdé"}) - {"foo/lorem/ipsum"} ) def test_yaml_meta(app_with_context, flatpages_factory): pages = flatpages_factory(app_with_context) foo = pages.get("foo") assert foo.meta == { "title": "Foo > bar", "created": datetime.date(2010, 12, 11), "updated": datetime.datetime(2015, 2, 9, 10, 59, 0), "updated_iso": datetime.datetime(2015, 2, 9, 10, 59, 0, tzinfo=utc), "versions": [3.14, 42], } assert foo["title"] == "Foo > bar" assert foo["created"] == datetime.date(2010, 12, 11) assert foo["updated"] == datetime.datetime(2015, 2, 9, 10, 59, 0) assert foo["updated_iso"] == datetime.datetime( 2015, 2, 9, 10, 59, 0, tzinfo=utc ) assert foo["versions"] == [3.14, 42] with pytest.raises(KeyError): foo["nonexistent"] def test_no_meta(app_with_context, temp_pages): pages = temp_pages(app_with_context) no_meta = pages.get("meta_styles/no_meta") assert no_meta.meta == {} filename = os.path.join(pages.root, "meta_styles", "no_meta.html") with open(filename, "w") as f_: f_.write("\n Hello, there's no metadata here.") pages.reload() no_meta = pages.get("meta_styles/no_meta") assert no_meta.meta == {} with open(filename, "w") as f_: f_.write("---\n---\nHello, there's no metadata here.") pages.reload() no_meta = pages.get("meta_styles/no_meta") assert no_meta.meta == {} with open(filename, "w") as f_: f_.write("---\n...\nHello, there's no metadata here.") pages.reload() no_meta = pages.get("meta_styles/no_meta") assert no_meta.meta == {} with open(filename, "w") as f_: f_.write("#Hello, there's no metadata here.") pages.reload() no_meta = pages.get("meta_styles/no_meta") assert no_meta.meta == {} def test_meta_closing_only(app_with_context, temp_pages): pages = temp_pages(app_with_context) page = pages.get("meta_styles/closing_block_only") assert page.meta == {"hello": "world"} filename = os.path.join( pages.root, "meta_styles", "closing_block_only.html" ) with open(filename, "w") as f: f.write("hello: world\n...\nFoo") pages.reload() page = pages.get("meta_styles/closing_block_only") assert page.meta == {"hello": "world"} def test_jekyll_style_meta(app_with_context, temp_pages): pages = temp_pages(app_with_context) jekyll_style = pages.get("meta_styles/jekyll_style") assert jekyll_style.meta == {"hello": "world"} assert jekyll_style.body == "Foo" filename = os.path.join(pages.root, "meta_styles", "jekyll_style.html") with open(filename, "r") as f_: lines = f_.readlines() with open(filename, "w") as f_: f_.write("\n".join(lines[1:])) pages.reload() jekyll_style = pages.get("meta_styles/jekyll_style") assert jekyll_style.meta == {"hello": "world"} assert jekyll_style.body == "Foo" def test_yaml_style_meta(app_with_context, temp_pages): pages = temp_pages(app_with_context) yaml_style = pages.get("meta_styles/yaml_style") yaml_style.meta == {"hello": "world"} yaml_style.body == "Foo" filename = os.path.join(pages.root, "meta_styles", "yaml_style.html") with open(filename, "r") as f_: lines = f_.readlines() with open(filename, "w") as f_: f_.write("\n".join(lines[1:])) pages.reload() yaml_style = pages.get("meta_styles/yaml_style") yaml_style.meta == {"hello": "world"} yaml_style.body == "Foo" def test_multi_line(app_with_context, flatpages_factory): pages = flatpages_factory(app_with_context) multi_line = pages.get("meta_styles/multi_line") assert multi_line.body == "Foo" assert "multi_line_string" in multi_line.meta assert "\n" in multi_line.meta["multi_line_string"] def test_parser_error(app_with_context, temp_pages): pages = temp_pages(app_with_context) with open(os.path.join(pages.root, "bad_file_test.html"), "w") as f: f.write("Hello World \u000b") with pytest.raises(yaml.reader.ReaderError, match=r".*bad_file_test.*"): pages.get("bad_file_test") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1767221560.0 flask_flatpages-0.9.0/tests/test_markdown_extensions.py0000644000175100017510000001415115125324470023175 0ustar00runnerrunner""" ============================== tests.test_markdown_extensions ============================== Test proper work of various Markdown extensions. """ from __future__ import annotations import sys import markdown import pytest from flask_flatpages import FlatPages from flask_flatpages.imports import PygmentsHtmlFormatter from markdown.extensions.toc import TocExtension def check_toc_page(pages: FlatPages): toc = pages.get("toc") fixture = markdown.markdown( toc.body, extensions=[TocExtension()], ) assert fixture == toc.html def check_default_codehilite_page(pages: FlatPages): codehilite = pages.get("codehilite") body = codehilite.body fixture = markdown.markdown( body, extensions=["codehilite"], ) assert codehilite.html == fixture def check_codehilite_with_linenums(pages: FlatPages): codehilite = pages.get("codehilite") body = codehilite.body fixture = markdown.markdown( body, extensions=["codehilite"], extension_configs={"codehilite": {"linenums": True}}, ) assert codehilite.html == fixture def check_extra(pages: FlatPages): extra_sep = "\n" if sys.version_info[:2] > (2, 6) else "\n\n" fixture = ( "

This is true markdown text.

\n" "
{0}" "

This is true markdown text.

\n" "
".format(extra_sep) ) extra = pages.get("extra") assert extra.html == fixture HELLO_HTML = "

Page Header

\n

Paragraph Header

\n

Text

" HELLO_ID_HTML = ( """

Page Header

\n""" """

Paragraph Header

\n""" """

Text

""" ) def test_default_markdown(app_with_context, flatpages_factory): pages = flatpages_factory(app_with_context) hello = pages.get("headerid") assert hello.html == HELLO_HTML app_with_context.config["FLATPAGES_MARKDOWN_EXTENSIONS"] = [] pages.reload() pages._file_cache = {} hello = pages.get("headerid") assert hello.html == HELLO_HTML if PygmentsHtmlFormatter is not None: check_default_codehilite_page(pages) @pytest.mark.skipif( PygmentsHtmlFormatter is None, reason="Pygments not installed" ) def test_codehilite_linenums_disabled(app_with_context, flatpages_factory): pages = flatpages_factory(app_with_context) # Test explicity disabled app_with_context.config["FLATPAGES_MARKDOWN_EXTENSIONS"] = ["codehilite"] check_default_codehilite_page(pages) # Test explicity disabled app_with_context.config["FLATPAGES_EXTENSION_CONFIGS"] = { "codehilite": {"linenums": "False"} } pages.reload() pages._file_cache = {} check_default_codehilite_page(pages) @pytest.mark.skipif( PygmentsHtmlFormatter is None, reason="Pygments not installed" ) def test_codehilite_linenums_enabled(app_with_context, flatpages_factory): pages = flatpages_factory(app_with_context) app_with_context.config["FLATPAGES_MARKDOWN_EXTENSIONS"] = ["codehilite"] app_with_context.config["FLATPAGES_EXTENSION_CONFIGS"] = { "codehilite": {"linenums": "True"} } check_codehilite_with_linenums(pages) def test_extra(app_with_context, flatpages_factory): pages = flatpages_factory(app_with_context) app_with_context.config["FLATPAGES_MARKDOWN_EXTENSIONS"] = ["extra"] check_extra(pages) def test_toc(app_with_context, flatpages_factory): pages = flatpages_factory(app_with_context) app_with_context.config["FLATPAGES_MARKDOWN_EXTENSIONS"] = ["toc"] check_toc_page(pages) def test_headerid_with_toc(app_with_context, flatpages_factory): pages = flatpages_factory(app_with_context) app_with_context.config["FLATPAGES_MARKDOWN_EXTENSIONS"] = [ "codehilite", "toc", # headerid is deprecated in Markdown 3.0 ] pages.reload() pages._file_cache = {} hello = pages.get("headerid") assert hello.html == HELLO_ID_HTML if PygmentsHtmlFormatter is not None: check_default_codehilite_page(pages) # test codehilite also loaded @pytest.mark.skipif( PygmentsHtmlFormatter is None, reason="Pygments not installed" ) def test_extension_importpath(app_with_context, flatpages_factory): pages = flatpages_factory(app_with_context) app_with_context.config["FLATPAGES_MARKDOWN_EXTENSIONS"] = [ "markdown.extensions.codehilite:CodeHiliteExtension" ] check_default_codehilite_page(pages) app_with_context.config[ "FLATPAGES_EXTENSION_CONFIGS" ] = { # Markdown 3 style config "markdown.extensions.codehilite:CodeHiliteExtension": { "linenums": True } } pages.reload() pages._file_cache = {} check_codehilite_with_linenums(pages) @pytest.mark.skipif( PygmentsHtmlFormatter is None, reason="Pygments not installed" ) def test_extension_object(app_with_context, flatpages_factory): pages = flatpages_factory(app_with_context) from markdown.extensions.codehilite import CodeHiliteExtension codehilite = CodeHiliteExtension() app_with_context.config["FLATPAGES_MARKDOWN_EXTENSIONS"] = [codehilite] check_default_codehilite_page(pages) codehilite = CodeHiliteExtension(linenums="True") # Check config applies app_with_context.config["FLATPAGES_MARKDOWN_EXTENSIONS"] = [codehilite] pages.reload() pages._file_cache = {} check_codehilite_with_linenums(pages) @pytest.mark.skipif( PygmentsHtmlFormatter is None, reason="Pygments not installed" ) def test_mixed_extension_types(app_with_context, flatpages_factory): from markdown.extensions.toc import TocExtension pages = flatpages_factory(app_with_context) toc = TocExtension() app_with_context.config["FLATPAGES_MARKDOWN_EXTENSIONS"] = [ toc, "codehilite", "markdown.extensions.extra:ExtraExtension", ] check_toc_page(pages) check_default_codehilite_page(pages) check_extra(pages) app_with_context.config["FLATPAGES_EXTENSION_CONFIGS"] = { "codehilite": {"linenums": "True"} } pages.reload() pages._file_cache = {} check_toc_page(pages) check_extra(pages) check_codehilite_with_linenums(pages)