Browse Source

Merge branch 'master' into module-support

Conflicts:
	CHANGES
	docs/_themes
	tests/flask_tests.py
pull/1638/head
Armin Ronacher 15 years ago
parent
commit
9fa4f94ad8
  1. 3
      .gitmodules
  2. 1
      CHANGES
  3. 1
      docs/_themes
  4. 344
      docs/_themes/flasky/static/flasky.css_t
  5. 3
      docs/_themes/flasky/theme.conf
  6. 2
      docs/api.rst
  7. 9
      docs/conf.py
  8. 82
      flask.py
  9. 87
      tests/flask_tests.py

3
.gitmodules vendored

@ -0,0 +1,3 @@
[submodule "docs/_themes"]
path = docs/_themes
url = git://github.com/mitsuhiko/flask-sphinx-themes.git

1
CHANGES

@ -15,6 +15,7 @@ Version 0.2
view function. view function.
- server listens on 127.0.0.1 by default now to fix issues with chrome. - server listens on 127.0.0.1 by default now to fix issues with chrome.
- added external URL support. - added external URL support.
- added support for :func:`~flask.send_file`
- module support and internal request handling refactoring - module support and internal request handling refactoring
to better support pluggable applications. to better support pluggable applications.

1
docs/_themes

@ -0,0 +1 @@
Subproject commit 11cb6b51c9ea3bc8f94afa3d7411b617f9db2570

344
docs/_themes/flasky/static/flasky.css_t vendored

@ -1,344 +0,0 @@
/*
* flasky.css_t
* ~~~~~~~~~~~~
*
* Sphinx stylesheet -- flasky theme based on nature theme.
*
* :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
@import url("basic.css");
/* -- page layout ----------------------------------------------------------- */
body {
font-family: 'Georgia', serif;
font-size: 17px;
background-color: #ddd;
color: #000;
margin: 0;
padding: 0;
}
div.document {
background: #fafafa;
}
div.documentwrapper {
float: left;
width: 100%;
}
div.bodywrapper {
margin: 0 0 0 230px;
}
hr {
border: 1px solid #B1B4B6;
}
div.body {
background-color: #ffffff;
color: #3E4349;
padding: 0 30px 30px 30px;
min-height: 34em;
}
img.floatingflask {
padding: 0 0 10px 10px;
float: right;
}
div.footer {
position: absolute;
right: 0;
margin-top: -70px;
text-align: right;
color: #888;
padding: 10px;
font-size: 14px;
}
div.footer a {
color: #888;
text-decoration: underline;
}
div.related {
line-height: 32px;
color: #888;
}
div.related ul {
padding: 0 0 0 10px;
}
div.related a {
color: #444;
}
div.sphinxsidebar {
font-size: 14px;
line-height: 1.5;
}
div.sphinxsidebarwrapper {
padding: 0 20px;
}
div.sphinxsidebarwrapper p.logo {
padding: 20px 0 10px 0;
margin: 0;
text-align: center;
}
div.sphinxsidebar h3,
div.sphinxsidebar h4 {
font-family: 'Garamond', 'Georgia', serif;
color: #222;
font-size: 24px;
font-weight: normal;
margin: 20px 0 5px 0;
padding: 0;
}
div.sphinxsidebar h4 {
font-size: 20px;
}
div.sphinxsidebar h3 a {
color: #444;
}
div.sphinxsidebar p {
color: #555;
margin: 10px 0;
}
div.sphinxsidebar ul {
margin: 10px 0;
padding: 0;
color: #000;
}
div.sphinxsidebar a {
color: #444;
text-decoration: none;
}
div.sphinxsidebar a:hover {
text-decoration: underline;
}
div.sphinxsidebar input {
border: 1px solid #ccc;
font-family: 'Georgia', serif;
font-size: 1em;
}
/* -- body styles ----------------------------------------------------------- */
a {
color: #004B6B;
text-decoration: underline;
}
a:hover {
color: #6D4100;
text-decoration: underline;
}
div.body {
padding-bottom: 40px; /* saved for footer */
}
div.body h1,
div.body h2,
div.body h3,
div.body h4,
div.body h5,
div.body h6 {
font-family: 'Garamond', 'Georgia', serif;
font-weight: normal;
margin: 30px 0px 10px 0px;
padding: 0;
}
div.body h1 { margin-top: 0; padding-top: 20px; font-size: 240%; }
div.body h2 { font-size: 180%; }
div.body h3 { font-size: 150%; }
div.body h4 { font-size: 130%; }
div.body h5 { font-size: 100%; }
div.body h6 { font-size: 100%; }
a.headerlink {
color: white;
padding: 0 4px;
text-decoration: none;
}
a.headerlink:hover {
color: #444;
background: #eaeaea;
}
div.body p, div.body dd, div.body li {
line-height: 1.4em;
}
div.admonition {
background: #fafafa;
margin: 20px -30px;
padding: 10px 30px;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
}
div.admonition p.admonition-title {
font-family: 'Garamond', 'Georgia', serif;
font-weight: normal;
font-size: 24px;
margin: 0 0 10px 0;
padding: 0;
line-height: 1;
}
div.admonition p.last {
margin-bottom: 0;
}
div.highlight{
background-color: white;
}
dt:target, .highlight {
background: #FAF3E8;
}
div.note {
background-color: #eee;
border: 1px solid #ccc;
}
div.seealso {
background-color: #ffc;
border: 1px solid #ff6;
}
div.topic {
background-color: #eee;
}
div.warning {
background-color: #ffe4e4;
border: 1px solid #f66;
}
p.admonition-title {
display: inline;
}
p.admonition-title:after {
content: ":";
}
pre, tt {
font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
font-size: 0.9em;
}
img.screenshot {
}
tt.descname, tt.descclassname {
font-size: 0.95em;
}
tt.descname {
padding-right: 0.08em;
}
img.screenshot {
-moz-box-shadow: 2px 2px 4px #eee;
-webkit-box-shadow: 2px 2px 4px #eee;
box-shadow: 2px 2px 4px #eee;
}
table.docutils {
border: 1px solid #888;
-moz-box-shadow: 2px 2px 4px #eee;
-webkit-box-shadow: 2px 2px 4px #eee;
box-shadow: 2px 2px 4px #eee;
}
table.docutils td, table.docutils th {
border: 1px solid #888;
padding: 0.25em 0.7em;
}
table.field-list, table.footnote {
border: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
}
table.footnote {
margin: 15px 0;
width: 100%;
border: 1px solid #eee;
}
table.field-list th {
padding: 0 0.8em 0 0;
}
table.field-list td {
padding: 0;
}
table.footnote td {
padding: 0.5em;
}
dl {
margin: 0;
padding: 0;
}
dl dd {
margin-left: 30px;
}
pre {
background: #eee;
padding: 7px 30px;
margin: 15px -30px;
line-height: 1.3em;
}
dl pre {
margin-left: -60px;
padding-left: 60px;
}
dl dl pre {
margin-left: -90px;
padding-left: 90px;
}
tt {
background-color: #ecf0f3;
color: #222;
/* padding: 1px 2px; */
}
tt.xref, a tt {
background-color: #FBFBFB;
}
a:hover tt {
background: #EEE;
}

3
docs/_themes/flasky/theme.conf vendored

@ -1,3 +0,0 @@
[theme]
inherit = basic
stylesheet = flasky.css

2
docs/api.rst

@ -222,6 +222,8 @@ Useful Functions and Classes
.. autofunction:: redirect .. autofunction:: redirect
.. autofunction:: send_file
.. autofunction:: escape .. autofunction:: escape
.. autoclass:: Markup .. autoclass:: Markup

9
docs/conf.py

@ -16,7 +16,7 @@ import sys, os
# If extensions (or modules to document with autodoc) are in another directory, # 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 # 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. # documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.append(os.path.abspath('.')) sys.path.append(os.path.abspath('_themes'))
# -- General configuration ----------------------------------------------------- # -- General configuration -----------------------------------------------------
@ -79,9 +79,6 @@ exclude_patterns = ['_build']
# output. They are ignored by default. # output. They are ignored by default.
#show_authors = False #show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'flaskext.FlaskyStyle'
# A list of ignored prefixes for module index sorting. # A list of ignored prefixes for module index sorting.
#modindex_common_prefix = [] #modindex_common_prefix = []
@ -90,7 +87,7 @@ pygments_style = 'flaskext.FlaskyStyle'
# The theme to use for HTML and HTML Help pages. Major themes that come with # The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'. # Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'flasky' html_theme = 'flask'
# Theme options are theme-specific and customize the look and feel of a theme # 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 # further. For a list of options available for each theme, see the
@ -237,3 +234,5 @@ intersphinx_mapping = {
'http://www.sqlalchemy.org/docs/': None, 'http://www.sqlalchemy.org/docs/': None,
'http://wtforms.simplecodes.com/docs/0.5/': None 'http://wtforms.simplecodes.com/docs/0.5/': None
} }
pygments_style = 'flask_theme_support.FlaskyStyle'

82
flask.py

@ -12,13 +12,14 @@
from __future__ import with_statement from __future__ import with_statement
import os import os
import sys import sys
import mimetypes
from datetime import datetime, timedelta from datetime import datetime, timedelta
from itertools import chain from itertools import chain
from jinja2 import Environment, PackageLoader, FileSystemLoader from jinja2 import Environment, PackageLoader, FileSystemLoader
from werkzeug import Request as RequestBase, Response as ResponseBase, \ from werkzeug import Request as RequestBase, Response as ResponseBase, \
LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \ LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \
ImmutableDict, cached_property ImmutableDict, cached_property, wrap_file, Headers
from werkzeug.routing import Map, Rule from werkzeug.routing import Map, Rule
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
from werkzeug.contrib.securecookie import SecureCookie from werkzeug.contrib.securecookie import SecureCookie
@ -268,6 +269,78 @@ def jsonify(*args, **kwargs):
indent=None if request.is_xhr else 2), mimetype='application/json') indent=None if request.is_xhr else 2), mimetype='application/json')
def send_file(filename_or_fp, mimetype=None, as_attachment=False,
attachment_filename=None):
"""Sends the contents of a file to the client. This will use the
most efficient method available and configured. By default it will
try to use the WSGI server's file_wrapper support. Alternatively
you can set the application's :attr:`~Flask.use_x_sendfile` attribute
to ``True`` to directly emit an `X-Sendfile` header. This however
requires support of the underlying webserver for `X-Sendfile`.
By default it will try to guess the mimetype for you, but you can
also explicitly provide one. For extra security you probably want
to sent certain files as attachment (HTML for instance).
Please never pass filenames to this function from user sources without
checking them first. Something like this is usually sufficient to
avoid security problems::
if '..' in filename or filename.startswith('/'):
abort(404)
.. versionadded:: 0.2
:param filename_or_fp: the filename of the file to send. This is
relative to the :attr:`~Flask.root_path` if a
relative path is specified.
Alternatively a file object might be provided
in which case `X-Sendfile` might not work and
fall back to the traditional method.
:param mimetype: the mimetype of the file if provided, otherwise
auto detection happens.
:param as_attachment: set to `True` if you want to send this file with
a ``Content-Disposition: attachment`` header.
:param attachment_filename: the filename for the attachment if it
differs from the file's filename.
"""
if isinstance(filename_or_fp, basestring):
filename = filename_or_fp
file = None
else:
file = filename_or_fp
filename = getattr(file, 'name', None)
if filename is not None:
filename = os.path.join(current_app.root_path, filename)
if mimetype is None and (filename or attachment_filename):
mimetype = mimetypes.guess_type(filename or attachment_filename)[0]
if mimetype is None:
mimetype = 'application/octet-stream'
headers = Headers()
if as_attachment:
if attachment_filename is None:
if filename is None:
raise TypeError('filename unavailable, required for '
'sending as attachment')
attachment_filename = os.path.basename(filename)
headers.add('Content-Disposition', 'attachment',
filename=attachment_filename)
if current_app.use_x_sendfile and filename:
if file is not None:
file.close()
headers['X-Sendfile'] = filename
data = None
else:
if file is None:
file = open(filename, 'rb')
data = wrap_file(request.environ, file)
return Response(data, mimetype=mimetype, headers=headers,
direct_passthrough=True)
def render_template(template_name, **context): def render_template(template_name, **context):
"""Renders a template from the template folder with the given """Renders a template from the template folder with the given
context. context.
@ -554,6 +627,13 @@ class Flask(_PackageBoundObject):
#: permanent session survive for roughly one month. #: permanent session survive for roughly one month.
permanent_session_lifetime = timedelta(days=31) permanent_session_lifetime = timedelta(days=31)
#: Enable this if you want to use the X-Sendfile feature. Keep in
#: mind that the server has to support this. This only affects files
#: sent with the :func:`send_file` method.
#:
#: .. versionadded:: 0.2
use_x_sendfile = False
#: options that are passed directly to the Jinja2 environment #: options that are passed directly to the Jinja2 environment
jinja_options = ImmutableDict( jinja_options = ImmutableDict(
autoescape=True, autoescape=True,

87
tests/flask_tests.py

@ -17,7 +17,8 @@ import flask
import unittest import unittest
import tempfile import tempfile
from datetime import datetime from datetime import datetime
from werkzeug import parse_date from werkzeug import parse_date, parse_options_header
from cStringIO import StringIO
example_path = os.path.join(os.path.dirname(__file__), '..', 'examples') example_path = os.path.join(os.path.dirname(__file__), '..', 'examples')
@ -474,6 +475,87 @@ class ModuleTestCase(unittest.TestCase):
assert app.test_client().get('/admin/').data == '42' assert app.test_client().get('/admin/').data == '42'
class SendfileTestCase(unittest.TestCase):
def test_send_file_regular(self):
app = flask.Flask(__name__)
with app.test_request_context():
rv = flask.send_file('static/index.html')
assert rv.direct_passthrough
assert rv.mimetype == 'text/html'
with app.open_resource('static/index.html') as f:
assert rv.data == f.read()
def test_send_file_xsendfile(self):
app = flask.Flask(__name__)
app.use_x_sendfile = True
with app.test_request_context():
rv = flask.send_file('static/index.html')
assert rv.direct_passthrough
assert 'x-sendfile' in rv.headers
assert rv.headers['x-sendfile'] == \
os.path.join(app.root_path, 'static/index.html')
assert rv.mimetype == 'text/html'
def test_send_file_object(self):
app = flask.Flask(__name__)
with app.test_request_context():
f = open(os.path.join(app.root_path, 'static/index.html'))
rv = flask.send_file(f)
with app.open_resource('static/index.html') as f:
assert rv.data == f.read()
assert rv.mimetype == 'text/html'
app.use_x_sendfile = True
with app.test_request_context():
f = open(os.path.join(app.root_path, 'static/index.html'))
rv = flask.send_file(f)
assert rv.mimetype == 'text/html'
assert 'x-sendfile' in rv.headers
assert rv.headers['x-sendfile'] == \
os.path.join(app.root_path, 'static/index.html')
app.use_x_sendfile = False
with app.test_request_context():
f = StringIO('Test')
rv = flask.send_file(f)
assert rv.data == 'Test'
assert rv.mimetype == 'application/octet-stream'
f = StringIO('Test')
rv = flask.send_file(f, mimetype='text/plain')
assert rv.data == 'Test'
assert rv.mimetype == 'text/plain'
app.use_x_sendfile = True
with app.test_request_context():
f = StringIO('Test')
rv = flask.send_file(f)
assert 'x-sendfile' not in rv.headers
def test_attachment(self):
app = flask.Flask(__name__)
with app.test_request_context():
f = open(os.path.join(app.root_path, 'static/index.html'))
rv = flask.send_file(f, as_attachment=True)
value, options = parse_options_header(rv.headers['Content-Disposition'])
assert value == 'attachment'
with app.test_request_context():
assert options['filename'] == 'index.html'
rv = flask.send_file('static/index.html', as_attachment=True)
value, options = parse_options_header(rv.headers['Content-Disposition'])
assert value == 'attachment'
assert options['filename'] == 'index.html'
with app.test_request_context():
rv = flask.send_file(StringIO('Test'), as_attachment=True,
attachment_filename='index.txt')
assert rv.mimetype == 'text/plain'
value, options = parse_options_header(rv.headers['Content-Disposition'])
assert value == 'attachment'
assert options['filename'] == 'index.txt'
def suite(): def suite():
from minitwit_tests import MiniTwitTestCase from minitwit_tests import MiniTwitTestCase
from flaskr_tests import FlaskrTestCase from flaskr_tests import FlaskrTestCase
@ -481,11 +563,12 @@ def suite():
suite.addTest(unittest.makeSuite(ContextTestCase)) suite.addTest(unittest.makeSuite(ContextTestCase))
suite.addTest(unittest.makeSuite(BasicFunctionalityTestCase)) suite.addTest(unittest.makeSuite(BasicFunctionalityTestCase))
suite.addTest(unittest.makeSuite(TemplatingTestCase)) suite.addTest(unittest.makeSuite(TemplatingTestCase))
suite.addTest(unittest.makeSuite(SendfileTestCase))
suite.addTest(unittest.makeSuite(ModuleTestCase))
if flask.json_available: if flask.json_available:
suite.addTest(unittest.makeSuite(JSONTestCase)) suite.addTest(unittest.makeSuite(JSONTestCase))
suite.addTest(unittest.makeSuite(MiniTwitTestCase)) suite.addTest(unittest.makeSuite(MiniTwitTestCase))
suite.addTest(unittest.makeSuite(FlaskrTestCase)) suite.addTest(unittest.makeSuite(FlaskrTestCase))
suite.addTest(unittest.makeSuite(ModuleTestCase))
return suite return suite

Loading…
Cancel
Save