From cf29877075a21d5fc4c08780f09bfa9865ad1586 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sat, 26 Jun 2010 12:04:26 +0800 Subject: [PATCH 001/207] docs/index.rst: Make alt text more informative. --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index e8ea5c33..16070051 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,7 +4,7 @@ Welcome to Flask ================ .. image:: _static/logo-full.png - :alt: The Flask Logo with Subtitle + :alt: Flask: web development, one drop at a time :class: floatingflask Welcome to Flask's documentation. This documentation is divided in From 14fde0779851866ee00a12b4353f1091d3999428 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sat, 26 Jun 2010 12:14:45 +0800 Subject: [PATCH 002/207] Copy edited the front page of the documentation. --- docs/index.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 16070051..c4ded1fe 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,17 +7,17 @@ Welcome to Flask :alt: Flask: web development, one drop at a time :class: floatingflask -Welcome to Flask's documentation. This documentation is divided in -different parts. I would suggest to get started with the -:ref:`installation` and then heading over to the :ref:`quickstart`. +Welcome to Flask's documentation. This documentation is divided into +different parts. I recommend that you get started with +:ref:`installation` and then head over to the :ref:`quickstart`. Besides the quickstart there is also a more detailed :ref:`tutorial` that shows how to create a complete (albeit small) application with Flask. If -you rather want to dive into all the internal parts of Flask, check out +you'd rather dive into the internals of Flask, check out the :ref:`api` documentation. Common patterns are described in the :ref:`patterns` section. -Flask also depends on two external libraries: the `Jinja2`_ template -engine and the `Werkzeug`_ WSGI toolkit. both of which are not documented +Flask depends on two external libraries: the `Jinja2`_ template +engine and the `Werkzeug`_ WSGI toolkit. These libraries are not documented here. If you want to dive into their documentation check out the following links: From f3dd3da59e269cdf50839d9b0e86fa2849516483 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sat, 26 Jun 2010 13:27:59 +0800 Subject: [PATCH 003/207] Copy edited and partially rewrote the foreword. --- docs/contents.rst.inc | 6 +-- docs/foreword.rst | 98 +++++++++++++++++++++---------------------- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index f0c21010..e924202e 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -1,9 +1,9 @@ User's Guide ------------ -This part of the documentation is written text and should give you an idea -how to work with Flask. It's a series of step-by-step instructions for -web development. +This part of the documentation, which is mostly prose, begins with some +background information about Flask, then focuses on step-by-step +instructions for web development with Flask. .. toctree:: :maxdepth: 2 diff --git a/docs/foreword.rst b/docs/foreword.rst index fe466dab..de6b4980 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -2,90 +2,90 @@ Foreword ======== Read this before you get started with Flask. This hopefully answers some -questions about the intention of the project, what it aims at and when you +questions about the purpose and goals of the project, and when you should or should not be using it. -What does Micro Mean? ---------------------- +What does "micro" mean? +----------------------- -The micro in microframework for me means on the one hand being small in -size and complexity but on the other hand also that the complexity of the -applications that are written with these frameworks do not exceed a -certain size. A microframework like Flask sacrifices a few things in -order to be approachable and to be as concise as possible. +To me, the "micro" in microframework refers not only to the simplicity and +small size of the framework, but also to the typically limited complexity +and size of applications that are written with the framework. To be +approachable and concise, a microframework sacrifices a few features that +may be necessary in larger or more complex applications. -For example Flask uses thread local objects internally so that you don't +For example, Flask uses thread-local objects internally so that you don't have to pass objects around from function to function within a request in order to stay threadsafe. While this is a really easy approach and saves you a lot of time, it also does not scale well to large applications. -It's especially painful for more complex unittests and when you suddenly -have to deal with code being executed outside of the context of a request -(for example if you have cronjobs). +It's especially painful for more complex unittests, and when you suddenly +have to deal with code being executed outside of the context of a request, +such as in cron jobs. -Flask provides some tools to deal with the downsides of this approach but -the core problem of this approach obviously stays. It is also based on -convention over configuration which means that a lot of things are -preconfigured in Flask and will work well for smaller applications but not -so much for larger ones (where and how it looks for templates, static -files etc.) +Flask provides some tools to deal with the downsides of this approach, but +the core problem remains. Flask is also based on convention over +configuration, which means that many things are preconfigured and will +work well for smaller applications but not so well for larger ones. For +example, by convention, templates and static files are in subdirectories +within the Python source tree of the application. -But don't worry if your application suddenly grows larger than it was -initially and you're afraid Flask might not grow with it. Even with -larger frameworks you sooner or later will find out that you need +But don't worry if your application suddenly grows larger +and you're afraid Flask might not grow with it. Even with +larger frameworks, you'll eventually discover that you need something the framework just cannot do for you without modification. If you are ever in that situation, check out the :ref:`becomingbig` chapter. -A Framework and An Example +A Framework and an Example -------------------------- -Flask is not only a microframework, it is also an example. Based on +Flask is not only a microframework; it is also an example. Based on Flask, there will be a series of blog posts that explain how to create a framework. Flask itself is just one way to implement a framework on top -of existing libraries. Unlike many other microframeworks Flask does not -try to implement anything on its own, it reuses existing code. +of existing libraries. Unlike many other microframeworks, Flask does not +try to implement everything on its own; it reuses existing code. Web Development is Dangerous ---------------------------- -I'm not even joking. Well, maybe a little. If you write a web -application you are probably allowing users to register and leave their +I'm not joking. Well, maybe a little. If you write a web +application, you are probably allowing users to register and leave their data on your server. The users are entrusting you with data. And even if you are the only user that might leave data in your application, you still -want that data to be stored in a secure manner. +want that data to be stored securely. -Unfortunately there are many ways security of a web application can be +Unfortunately, there are many ways the security of a web application can be compromised. Flask protects you against one of the most common security -problems of modern web applications: cross site scripting (XSS). Unless -you deliberately mark insecure HTML as secure Flask (and the underlying -Jinja2 template engine) have you covered. But there are many more ways to +problems of modern web applications: cross-site scripting (XSS). Unless +you deliberately mark insecure HTML as secure, Flask and the underlying +Jinja2 template engine have you covered. But there are many more ways to cause security problems. -Whenever something is dangerous where you have to watch out, the -documentation will tell you so. Some of the security concerns of web -development are far more complex than one might think and often we all end -up in situations where we think "well, this is just far fetched, how could -that possibly be exploited" and then an intelligent guy comes along and -figures a way out to exploit that application. And don't think, your -application is not important enough for hackers to take notice. Depending -on the kind of attack, chances are there are automated botnets out there -trying to figure out how to fill your database with viagra advertisements. +The documentation will warn you about aspects of web development that +require attention to security. Some of these security concerns +are far more complex than one might think, and we all sometimes underestimate +the likelihood that a vulnerability will be exploited, until a clever +attacker figures out a way to exploit our applications. And don't think +that your application is not important enough to attract an attacker. +Depending on the kind of attack, chances are that automated bots are +probing for ways to fill your database with spam, links to malicious +software, and the like. -So always keep that in mind when doing web development. +So always keep security in mind when doing web development. Target Audience --------------- -Is Flask for you? If your application small-ish and does not depend on -too complex database structures, Flask is the Framework for you. It was -designed from the ground up to be easy to use, based on established -principles, good intentions and on top of two established libraries in -widespread usage. Recent versions of Flask scale nicely within reasonable -bounds and if you grow larger, you won't have any troubles adjusting Flask +Is Flask for you? If your application is small-ish and does not depend on +very complex database structures, Flask is the Framework for you. It was +designed from the ground up to be easy to use, and built on the firm +foundation of established principles, good intentions, and mature, widely +used libraries. Recent versions of Flask scale nicely within reasonable +bounds, and if you grow larger, you won't have any trouble adjusting Flask for your new application size. If you suddenly discover that your application grows larger than originally intended, head over to the :ref:`becomingbig` section to see some possible solutions for larger applications. -Satisfied? Then head over to the :ref:`installation`. +Satisfied? Then let's proceed with :ref:`installation`. From f5e533076fb6f0102bcba9ddb24f905045fa6504 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sat, 26 Jun 2010 14:39:16 +0800 Subject: [PATCH 004/207] Edited the installation guide. --- docs/installation.rst | 137 ++++++++++++++++++++---------------------- 1 file changed, 66 insertions(+), 71 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 78fb103f..040412c6 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -3,44 +3,41 @@ Installation ============ -Flask is a microframework and yet it depends on external libraries. There -are various ways how you can install that library and this explains each -way and why there are multiple ways. - -Flask depends on two external libraries: `Werkzeug +Flask depends on two external libraries, `Werkzeug `_ and `Jinja2 `_. -The first one is responsible for interfacing WSGI the latter for rendering -templates. Now you are maybe asking, what is WSGI? WSGI is a standard -in Python that is basically responsible for ensuring that your application -is behaving in a specific way so that you can run it on different -environments (for example on a local development server, on an Apache2, on -lighttpd, on Google's App Engine or whatever you have in mind). +Werkzeug is a toolkit for WSGI, the standard Python interface between web +applications and a variety of servers for both development and deployment. +Jinja2 renders templates. -So how do you get all that on your computer in no time? The most kick-ass -method is virtualenv, so let's look at that first. +So how do you get all that on your computer quickly? There are many ways +which this section will explain, but the most kick-ass method is +virtualenv, so let's look at that first. .. _virtualenv: virtualenv ---------- -Virtualenv is what you want to use during development and in production if -you have shell access. So first: what does virtualenv do? If you are -like me and you like Python, chances are you want to use it for another -project as well. Now the more projects you have, the more likely it is -that you will be working with different versions of Python itself or at -least an individual library. Because let's face it: quite often libraries -break backwards compatibility and it's unlikely that your application will -not have any dependencies, that just won't happen. So virtualenv to the -rescue! +Virtualenv is probably what you want to use during development, and in +production too if you have shell access there. -It basically makes it possible to have multiple side-by-side -"installations" of Python, each for your own project. It's not actually -an installation but a clever way to keep things separated. +What problem does virtualenv solve? If you like Python as I do, +chances are you want to use it for other projects besides Flask-based +web applications. But the more projects you have, the more likely it is +that you will be working with different versions of Python itself, or at +least different versions of Python libraries. Let's face it; quite often +libraries break backwards compatibility, and it's unlikely that any serious +application will have zero dependencies. So what do you do if two or more +of your projects have conflicting dependencies? -So let's see how that works! +Virtualenv to the rescue! It basically enables multiple side-by-side +installations of Python, one for each project. It doesn't actually +install separate copies of Python, but it does provide a clever way +to keep different project environments isolated. -If you are on OS X or Linux chances are that one of the following two +So let's see how virtualenv works! + +If you are on Mac OS X or Linux, chances are that one of the following two commands will work for you:: $ sudo easy_install virtualenv @@ -49,18 +46,19 @@ or even better:: $ sudo pip install virtualenv -Chances are you have virtualenv installed on your system then. Maybe it's -even in your package manager (on ubuntu try ``sudo apt-get install -python-virtualenv``). +One of these will probably install virtualenv on your system. Maybe it's +even in your package manager. If you use Ubuntu, try:: -If you are on Windows and missing the `easy_install` command you have to + $ sudo apt-get install python-virtualenv + +If you are on Windows and don't have the `easy_install` command, you must install it first. Check the :ref:`windows-easy-install` section for more information about how to do that. Once you have it installed, run the -same commands as above, but without the `sudo` part. +same commands as above, but without the `sudo` prefix. -So now that you have virtualenv running just fire up a shell and create -your own environment. I usually create a folder and a `env` folder -within:: +Once you have virtualenv installed, just fire up a shell and create +your own environment. I usually create a project folder and an `env` +folder within:: $ mkdir myproject $ cd myproject @@ -68,14 +66,14 @@ within:: New python executable in env/bin/python Installing setuptools............done. -Now you only have to activate it, whenever you work with it. On OS X and -Linux do the following:: +Now, whenever you want to work on a project, you only have to activate +the corresponding environment. On OS X and Linux, do the following:: $ . env/bin/activate -(Note the whitespace between the dot and the script name. This means -execute this file in context of the shell. If the dot does not work for -whatever reason in your shell, try substituting it with ``source``) +(Note the space between the dot and the script name. The dot means that +this script should run in the context of the current shell. If this command +does not work in your shell, try replacing the dot with ``source``) If you are a Windows user, the following command is for you:: @@ -95,23 +93,22 @@ A few seconds later you are good to go. System Wide Installation ------------------------ -This is possible as well, but I would not recommend it. Just run +This is possible as well, but I do not recommend it. Just run `easy_install` with root rights:: - sudo easy_install Flask + $ sudo easy_install Flask -(Run it in an Admin shell on Windows systems and without the `sudo`). +(Run it in an Admin shell on Windows systems and without `sudo`). Living on the Edge ------------------ -You want to work with the latest version of Flask, there are two ways: you -can either let `easy_install` pull in the development version or tell it -to operate on a git checkout. Either way it's recommended to do that in a -virtualenv. +If you want to work with the latest version of Flask, there are two ways: you +can either let `easy_install` pull in the development version, or tell it +to operate on a git checkout. Either way, virtualenv is recommended. -Get the git checkout in a new virtualenv and run in develop mode:: +Get the git checkout in a new virtualenv and run in development mode:: $ git clone http://github.com/mitsuhiko/flask.git Initialized empty Git repository in ~/dev/flask/.git/ @@ -124,9 +121,9 @@ Get the git checkout in a new virtualenv and run in develop mode:: ... Finished processing dependencies for Flask -This will pull in the dependencies and activate the git head as current -version. Then you just have to ``git pull origin`` to get the latest -version. +This will pull in the dependencies and activate the git head as the current +version inside the virtualenv. Then you just have to ``git pull origin`` +to get the latest version. To just get the development version without git, do this instead:: @@ -145,31 +142,29 @@ To just get the development version without git, do this instead:: `easy_install` on Windows ------------------------- -On Windows installation of `easy_install` is a little bit tricker because -on Windows slightly different rules apply, but it's not a biggy. The -easiest way to accomplish that is downloading the `ez_setup.py`_ file and -running it. (Double clicking should do the trick) +On Windows, installation of `easy_install` is a little bit tricker because +slightly different rules apply on Windows than on Unix-like systems, but +it's not difficult. The easiest way to do it is to download the +`ez_setup.py`_ file and run it. The easiest way to run the file is to +open your downloads folder and double-click on the file. -Once you have done that it's important to add the `easy_install` command -and other Python scripts to the path. To do that you have to add the -Python installation's Script folder to the `PATH` variable. - -To do that, right-click on your "Computer" desktop icon and click -"Properties". On Windows Vista and Windows 7 then click on "Advanced System -settings", on Windows XP click on the "Advanced" tab instead. Then click +Next, add the `easy_install` command and other Python scripts to the +command search path, by adding your Python installation's Scripts folder +to the `PATH` environment variable. To do that, right-click on the +"Computer" icon on the Desktop or in the Start menu, and choose +"Properties". Then, on Windows Vista and Windows 7 click on "Advanced System +settings"; on Windows XP, click on the "Advanced" tab instead. Then click on the "Environment variables" button and double click on the "Path" -variable in the "System variables" section. - -There append the path of your Python interpreter's Script folder to the -end of the last (make sure you delimit it from existing values with a -semicolon). Assuming you are using Python 2.6 on the default path, add -the following value:: +variable in the "System variables" section. There append the path of your +Python interpreter's Scripts folder; make sure you delimit it from +existing values with a semicolon. Assuming you are using Python 2.6 on +the default path, add the following value:: ;C:\Python26\Scripts -Then you are done. To check that it worked, open the cmd and execute -"easy_install". If you have UAC enabled it should prompt you for admin -privileges. +Then you are done. To check that it worked, open the Command Prompt and +execute ``easy_install``. If you have User Account Control enabled on +Windows Vista or Windows 7, it should prompt you for admin privileges. .. _ez_setup.py: http://peak.telecommunity.com/dist/ez_setup.py From 4a0c0446da3fc278a5cdc2587191f6df66489d02 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 26 Jun 2010 03:07:19 -0700 Subject: [PATCH 005/207] Added Matt Campell --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index c3c0e13b..a3ddd0f2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,6 +16,7 @@ Patches and Suggestions - Justin Quick - Kenneth Reitz - Marian Sigler +- Matt Campell - Matthew Frazier - Ron DuPlain - Sebastien Estienne From 67f3f1bac5cfadfa5398e18dd232d279576cf85c Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sat, 26 Jun 2010 22:43:14 +0800 Subject: [PATCH 006/207] Track the docs/_themes directory --- docs/_themes | 1 + 1 file changed, 1 insertion(+) create mode 160000 docs/_themes diff --git a/docs/_themes b/docs/_themes new file mode 160000 index 00000000..b8e0f4f1 --- /dev/null +++ b/docs/_themes @@ -0,0 +1 @@ +Subproject commit b8e0f4f1bfc7c89fffb5440fcdf60edaa033c836 From 824c10c6e4d9c2235d41a2276a0158b1456c3b06 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sun, 27 Jun 2010 17:37:04 +0800 Subject: [PATCH 007/207] Added MongoKit pattern and fixed typo in sqlalchemy pattern --- docs/patterns/mongokit.rst | 133 +++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 docs/patterns/mongokit.rst diff --git a/docs/patterns/mongokit.rst b/docs/patterns/mongokit.rst new file mode 100644 index 00000000..5f6b3b94 --- /dev/null +++ b/docs/patterns/mongokit.rst @@ -0,0 +1,133 @@ +.. mongokit-pattern: + +MongoKit in Flask +================= + +Using a document database rather than a full DBMS gets more common these days. +This pattern shows how to use MongoKit, a document mapper library, to +integrate with MongoDB. + +This pattern requires an running MongoDB server and the MongoKit library +installed. + +There are two very common ways to use MongoKit. I will outline each of them +here: + + +Declarative +----------- + +The default behaviour of MongoKit is the declarative one that is based on +common ideas from Django or the SQLAlchemy declarative extension. + +Here an example `database.py` module for your application:: + + from mongokit import Connection, Document + + connection = Connection() + + +To define your models, just subclass the `Document` class that is imported +from MongoKit. If you've seen the SQLAlchemy pattern you may wonder why we do +not have a session and even do not define a `init_db` function here. On the +one hand, MongoKit does not have something like a session. This sometimes +makes it more to type but also makes it blazingly fast. On the other hand, +MongoDB is schemaless. This means you can modify the data structure from one +insert query to the next without any problem. MongoKit is just schemaless +too, but implements some validation to ensure data integrity. + +Here is an example document (put this into `models.py`, e.g.):: + + from yourapplication.database import Document, connection + + def max_length(length): + def validate(value): + if len(value) <= length: + return True + raise Exception('%s must be at most %s characters long' % length) + return validate + + class User(Document): + structure = { + 'name': unicode, + 'email': unicode, + } + validators = { + 'name': max_length(50), + 'email': max_length(120) + } + use_dot_notation = True + def __repr__(self): + return '' % (self.name) + + # register the User document with our current connection + connection.register([User]) + + +This example shows you how to define your schema (named structure), a +validator for the maximum character length and uses a special MongoKit feature +called `use_dot_notation`. Per default MongoKit behaves like a python +dictionary but with `use_dot_notation` set to `True` you can use your +documents like you use models in nearly any other ORM by using dots to +seperate between attributes. + +You can insert entries into the database like this: + +>>> from yourapplication.database import connection +>>> from yourapplication.models import User +>>> collection = connection['test'].users +>>> user = collection.User() +>>> user['name'] = u'admin' +>>> user['email'] = u'admin@localhost' +>>> user.save() + +Note that MongoKit is kinda strict with used column types, you must not use a +common `str` type for either `name` or `email` but unicode. + +Querying is simple as well: + +>>> list(collection.User.find()) +[] +>>> collection.User.find_one({'name': u'admin'}) + + +.. _MongoKit: http://bytebucket.org/namlook/mongokit/ + + +PyMongo Compatibility Layer +--------------------------- + +If you just want to use PyMongo, you can do that with MongoKit as well. You +may use this process if you need the best performance to get:: + + from MongoKit import Connection + + connection = Connection() + +To insert data you can use the `insert` method. We have to get a +collection first, this is somewhat the same as a table in the SQL world. + +>>> collection = connection['test'].users +>>> user = {'name': u'admin', 'email': u'admin@localhost'} +>>> collection.insert(user) + +print list(collection.find()) +print collection.find_one({'name': u'admin'}) + +MongoKit will automatically commit for us. + +To query your database, you use the collection directly: + +>>> list(collection.find()) +[{u'_id': ObjectId('4c271729e13823182f000000'), u'name': u'admin', u'email': u'admin@localhost'}] +>>> collection.find_one({'name': u'admin'}) +{u'_id': ObjectId('4c271729e13823182f000000'), u'name': u'admin', u'email': u'admin@localhost'} + +These results are also dict-like objects: + +>>> r = collection.find_one({'name': u'admin'}) +>>> r['email'] +u'admin@localhost' + +For more information about MongoKit, head over to the +`website `_. From b5db6bf529706da651389ef51b0fce87a89a47e8 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sun, 27 Jun 2010 17:38:39 +0800 Subject: [PATCH 008/207] now really pushed the fix in sqlalchemy pattern --- docs/patterns/index.rst | 1 + docs/patterns/sqlalchemy.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 2a8c0af7..23b20dc5 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -30,3 +30,4 @@ Snippet Archives `_. jquery errorpages lazyloading + mongokit diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 3945c1fa..6a14c8e0 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -68,7 +68,7 @@ Here is an example model (put this into `models.py`, e.g.):: self.email = email def __repr__(self): - return '' % (self.name, self.email) + return '' % (self.name) You can insert entries into the database like this: From 87c2c794421f8a39aa98a35f518613cfd38852d5 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sun, 27 Jun 2010 22:03:43 +0800 Subject: [PATCH 009/207] fixed mongokit pattern to show how the flask way to configure hostname and port --- docs/patterns/mongokit.rst | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/patterns/mongokit.rst b/docs/patterns/mongokit.rst index 5f6b3b94..27f182a3 100644 --- a/docs/patterns/mongokit.rst +++ b/docs/patterns/mongokit.rst @@ -20,11 +20,22 @@ Declarative The default behaviour of MongoKit is the declarative one that is based on common ideas from Django or the SQLAlchemy declarative extension. -Here an example `database.py` module for your application:: +Here an example `app.py` module for your application:: + from flask import Flask from mongokit import Connection, Document - connection = Connection() + # configuration + MONGODB_HOST = 'localhost' + MONGODB_PORT = 27017 + + # create the little application object + app = Flask(__name__) + app.config.from_object(__name__) + + # connect to the database + connection = Connection(app.config['MONGODB_HOST'], + app.config['MONGODB_PORT']) To define your models, just subclass the `Document` class that is imported @@ -36,10 +47,8 @@ MongoDB is schemaless. This means you can modify the data structure from one insert query to the next without any problem. MongoKit is just schemaless too, but implements some validation to ensure data integrity. -Here is an example document (put this into `models.py`, e.g.):: +Here is an example document (put this also into `app.py`, e.g.):: - from yourapplication.database import Document, connection - def max_length(length): def validate(value): if len(value) <= length: @@ -98,7 +107,9 @@ PyMongo Compatibility Layer --------------------------- If you just want to use PyMongo, you can do that with MongoKit as well. You -may use this process if you need the best performance to get:: +may use this process if you need the best performance to get. Note that this +example does not show how to couple it with Flask, see the above MongoKit code +for examples:: from MongoKit import Connection From 5ede53066fc26d993dd229fe0c54209e700f4c09 Mon Sep 17 00:00:00 2001 From: Adam Zapletal Date: Mon, 28 Jun 2010 11:10:07 +0800 Subject: [PATCH 010/207] Minor tutorial documentation fixes (grammar, etc) --- docs/tutorial/setup.rst | 4 ++-- docs/tutorial/views.rst | 28 ++++++++++++++-------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst index 790cc41e..b5ae2a0e 100644 --- a/docs/tutorial/setup.rst +++ b/docs/tutorial/setup.rst @@ -52,7 +52,7 @@ The `secret_key` is needed to keep the client-side sessions secure. Choose that key wisely and as hard to guess and complex as possible. The debug flag enables or disables the interactive debugger. Never leave debug mode activated in a production system because it will allow users to -executed code on the server! +execute code on the server! We also add a method to easily connect to the database specified. That can be used to open a connection on request and also from the interactive @@ -64,7 +64,7 @@ Python shell or a script. This will come in handy later return sqlite3.connect(app.config['DATABASE']) Finally we just add a line to the bottom of the file that fires up the -server if we run that file as standalone application:: +server if we want to run that file as a standalone application:: if __name__ == '__main__': app.run() diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst index c7cc1ed4..0bce03a3 100644 --- a/docs/tutorial/views.rst +++ b/docs/tutorial/views.rst @@ -11,11 +11,11 @@ Show Entries This view shows all the entries stored in the database. It listens on the root of the application and will select title and text from the database. -The one with the highest id (the newest entry) on top. The rows returned -from the cursor are tuples with the columns ordered like specified in the -select statement. This is good enough for small applications like here, -but you might want to convert them into a dict. If you are interested how -to do that, check out the :ref:`easy-querying` example. +The one with the highest id (the newest entry) will be on top. The rows +returned from the cursor are tuples with the columns ordered like specified +in the select statement. This is good enough for small applications like +here, but you might want to convert them into a dict. If you are +interested in how to do that, check out the :ref:`easy-querying` example. The view function will pass the entries as dicts to the `show_entries.html` template and return the rendered one:: @@ -53,11 +53,11 @@ Login and Logout These functions are used to sign the user in and out. Login checks the username and password against the ones from the configuration and sets the -`logged_in` key in the session. If the user logged in successfully that -key is set to `True` and the user is redirected back to the `show_entries` -page. In that case also a message is flashed that informs the user he or -she was logged in successfully. If an error occoured the template is -notified about that and the user asked again:: +`logged_in` key in the session. If the user logged in successfully, that +key is set to `True`, and the user is redirected back to the `show_entries` +page. In addition, a message is flashed that informs the user that he or +she was logged in successfully. If an error occurred, the template is +notified about that, and the user is asked again:: @app.route('/login', methods=['GET', 'POST']) def login(): @@ -73,12 +73,12 @@ notified about that and the user asked again:: return redirect(url_for('show_entries')) return render_template('login.html', error=error) -The logout function on the other hand removes that key from the session +The logout function, on the other hand, removes that key from the session again. We use a neat trick here: if you use the :meth:`~dict.pop` method -of the dict and pass a second parameter to it (the default) the method +of the dict and pass a second parameter to it (the default), the method will delete the key from the dictionary if present or do nothing when that -key was not in there. This is helpful because we don't have to check in -that case if the user was logged in. +key is not in there. This is helpful because now we don't have to check +if the user was logged in. :: From 505c530c9a4c8f1b6b063bcb481a4fdcf3897709 Mon Sep 17 00:00:00 2001 From: Adam Zapletal Date: Mon, 28 Jun 2010 11:24:37 +0800 Subject: [PATCH 011/207] Minor testing documentation fixes (grammar, etc) --- docs/testing.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index f9785b18..d131c673 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -8,8 +8,8 @@ Testing Flask Applications Not sure where that is coming from, and it's not entirely correct, but also not that far from the truth. Untested applications make it hard to improve existing code and developers of untested applications tend to -become pretty paranoid. If an application however has automated tests, you -can safely change things and you will instantly know if your change broke +become pretty paranoid. If an application has automated tests, you can +safely change things, and you will instantly know if your change broke something. Flask gives you a couple of ways to test applications. It mainly does @@ -60,7 +60,7 @@ each individual test function. To delete the database after the test, we close the file and remove it from the filesystem in the :meth:`~unittest.TestCase.tearDown` method. What the test client does is give us a simple interface to the application. We can trigger test -requests to the application and the client will also keep track of cookies +requests to the application, and the client will also keep track of cookies for us. Because SQLite3 is filesystem-based we can easily use the tempfile module @@ -130,7 +130,7 @@ Logging In and Out ------------------ The majority of the functionality of our application is only available for -the administration user. So we need a way to log our test client in to the +the administrative user, so we need a way to log our test client in to the application and out of it again. For that we fire some requests to the login and logout pages with the required form data (username and password). Because the login and logout pages redirect, we tell the @@ -200,12 +200,12 @@ suite. Other Testing Tricks -------------------- -Besides using the test client we used above there is also the +Besides using the test client we used above, there is also the :meth:`~flask.Flask.test_request_context` method that in combination with the `with` statement can be used to activate a request context temporarily. With that you can access the :class:`~flask.request`, :class:`~flask.g` and :class:`~flask.session` objects like in view -functions. Here a full example that showcases this:: +functions. Here's a full example that showcases this:: app = flask.Flask(__name__) From ee5eafa7951bf636bdf345cb072f89947e265867 Mon Sep 17 00:00:00 2001 From: Adam Zapletal Date: Mon, 28 Jun 2010 11:43:44 +0800 Subject: [PATCH 012/207] Error Handling documentation fixes (grammar, etc) --- docs/errorhandling.rst | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index cc7437a6..ac9595f1 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -5,7 +5,7 @@ Handling Application Errors .. versionadded:: 0.3 -Applications fail, server fail. Sooner or later you will see an exception +Applications fail, servers fail. Sooner or later you will see an exception in production. Even if your code is 100% correct, you will still see exceptions from time to time. Why? Because everything else involved will fail. Here some situations where perfectly fine code can lead to server @@ -20,7 +20,7 @@ errors: - a programming error in a library you are using - network connection of the server to another system failed. -And that's just a small sample of issues you could be facing. So how to +And that's just a small sample of issues you could be facing. So how do we deal with that sort of problem? By default if your application runs in production mode, Flask will display a very simple page for you and log the exception to the :attr:`~flask.Flask.logger`. @@ -32,10 +32,10 @@ Error Mails ----------- If the application runs in production mode (which it will do on your -server) you won't see any log messages by default. Why that? Flask tries -to be a zero-configuration framework and where should it drop the logs for -you if there is no configuration. Guessing is not a good idea because -chances are, the place it guessed is not the place where the user has the +server) you won't see any log messages by default. Why is that? Flask +tries to be a zero-configuration framework. Where should it drop the logs +for you if there is no configuration? Guessing is not a good idea because +chances are, the place it guessed is not the place where the user has permission to create a logfile. Also, for most small applications nobody will look at the logs anyways. @@ -45,9 +45,9 @@ when a user reported it for you. What you want instead is a mail the second the exception happened. Then you get an alert and you can do something about it. -Flask is using the Python builtin logging system and that one can actually -send you mails for errors which is probably what you want. Here is how -you can configure the Flask logger to send you mails for exceptions:: +Flask uses the Python builtin logging system, and it can actually send +you mails for errors which is probably what you want. Here is how you can +configure the Flask logger to send you mails for exceptions:: ADMINS = ['yourname@example.com'] if not app.debug: @@ -63,8 +63,9 @@ So what just happened? We created a new :class:`~logging.handlers.SMTPHandler` that will send mails with the mail server listening on ``127.0.0.1`` to all the `ADMINS` from the address *server-error@example.com* with the subject "YourApplication Failed". If -your mail server requires credentials these can also provided, for that -check out the documentation for the :class:`~logging.handlers.SMTPHandler`. +your mail server requires credentials, these can also be provided. For +that check out the documentation for the +:class:`~logging.handlers.SMTPHandler`. We also tell the handler to only send errors and more critical messages. Because we certainly don't want to get a mail for warnings or other @@ -115,12 +116,12 @@ Controlling the Log Format -------------------------- By default a handler will only write the message string into a file or -send you that message as mail. But a log record stores more information +send you that message as mail. A log record stores more information, and it makes a lot of sense to configure your logger to also contain that information so that you have a better idea of why that error happened, and more importantly, where it did. -A formatter can be instanciated with a format string. Note that +A formatter can be instantiated with a format string. Note that tracebacks are appended to the log entry automatically. You don't have to do that in the log formatter format string. @@ -206,7 +207,7 @@ formatter. The formatter has three interesting methods: called for `asctime` formatting. If you want a different time format you can override this method. :meth:`~logging.Formatter.formatException` - called for exception formatting. It is passed a :attr:`~sys.exc_info` + called for exception formatting. It is passed an :attr:`~sys.exc_info` tuple and has to return a string. The default is usually fine, you don't have to override it. @@ -217,8 +218,8 @@ Other Libraries --------------- So far we only configured the logger your application created itself. -Other libraries might log themselves as well. For example, SQLAlchemy use -logging heavily in the core. While there is a method to configure all +Other libraries might log themselves as well. For example, SQLAlchemy uses +logging heavily in its core. While there is a method to configure all loggers at once in the :mod:`logging` package, I would not recommend using it. There might be a situation in which you want to have multiple separate applications running side by side in the same Python interpreter From 24754114fc023c92385a1f9bfef141d93a9c8f9b Mon Sep 17 00:00:00 2001 From: Adam Zapletal Date: Mon, 28 Jun 2010 11:49:50 +0800 Subject: [PATCH 013/207] Minor config documentation fixes (grammar, etc) --- docs/config.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index e057ba4d..73d7c8fb 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -6,12 +6,12 @@ Configuration Handling .. versionadded:: 0.3 Applications need some kind of configuration. There are different things -you might want to change. Like toggling debug mode, the secret key and a +you might want to change like toggling debug mode, the secret key, and a lot of very similar things. The way Flask is designed usually requires the configuration to be -available when the application starts up. You can either hardcode the -configuration in the code which for many small applications is not +available when the application starts up. You can hardcode the +configuration in the code, which for many small applications is not actually that bad, but there are better ways. Independent of how you load your config, there is a config object @@ -64,8 +64,8 @@ The following configuration values are used internally by Flask: Configuring from Files ---------------------- -Configuration becomes more useful if you can configure from a file. And -ideally that file would be outside of the actual application package that +Configuration becomes more useful if you can configure from a file, and +ideally that file would be outside of the actual application package so that you can install the package with distribute (:ref:`distribute-deployment`) and still modify that file afterwards. @@ -75,7 +75,7 @@ So a common pattern is this:: app.config.from_object('yourapplication.default_settings') app.config.from_envvar('YOURAPPLICATION_SETTINGS') -What this does is first loading the configuration from the +This first loads the configuration from the `yourapplication.default_settings` module and then overrides the values with the contents of the file the :envvar:`YOURAPPLICATION_SETTINGS` environment variable points to. This environment variable can be set on @@ -95,7 +95,7 @@ The configuration files themselves are actual Python files. Only values in uppercase are actually stored in the config object later on. So make sure to use uppercase letters for your config keys. -Here an example configuration file:: +Here is an example configuration file:: DEBUG = False SECRET_KEY = '?\xbf,\xb4\x8d\xa3"<\x9c\xb0@\x0f5\xab,w\xee\x8d$0\x13\x8b83' From 4ed7a923f53c71339251dd1ccd681f424b061bd1 Mon Sep 17 00:00:00 2001 From: Adam Zapletal Date: Mon, 28 Jun 2010 11:52:32 +0800 Subject: [PATCH 014/207] Minor sqlite3 documentation fixes (grammar, etc) --- docs/patterns/sqlite3.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index c11e837d..1032097e 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -3,12 +3,12 @@ Using SQLite 3 with Flask ========================= -In Flask you can implement opening of database connections at the beginning -of the request and closing at the end with the +In Flask you can implement the opening of database connections at the +beginning of the request and closing at the end with the :meth:`~flask.Flask.before_request` and :meth:`~flask.Flask.after_request` decorators in combination with the special :class:`~flask.g` object. -So here a simple example of how you can use SQLite 3 with Flask:: +So here is a simple example of how you can use SQLite 3 with Flask:: import sqlite3 from flask import g @@ -33,7 +33,7 @@ Easy Querying ------------- Now in each request handling function you can access `g.db` to get the -current open database connection. To simplify working with SQLite a +current open database connection. To simplify working with SQLite, a helper function can be useful:: def query_db(query, args=(), one=False): From 55040d3efa2d608eb4daa1ed7086e0bd36cfd252 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 28 Jun 2010 00:13:48 -0700 Subject: [PATCH 015/207] Added Adam Zapletal and Christopher Grebs to AUTHORs file. --- AUTHORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index a3ddd0f2..e20a3735 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,8 +9,10 @@ Development Lead Patches and Suggestions ``````````````````````` +- Adam Zapletal - Chris Edgemon - Chris Grindstaff +- Christopher Grebs - Florent Xicluna - Georg Brandl - Justin Quick From f195d9244756b9809bd2b00601b437f3e278b0b9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 29 Jun 2010 01:13:40 +0200 Subject: [PATCH 016/207] Added proper subdomain support --- CHANGES | 4 ++++ docs/config.rst | 9 +++++++++ flask.py | 6 ++++-- tests/flask_tests.py | 21 +++++++++++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index d77a1f50..3490f0e2 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,10 @@ Version 0.5 Codename to be decided, release date to be announced. +- fixed a bug with subdomains that was caused by the inability to + specify the server name. The server name can now be set with + the `SERVER_NAME` config key. + Version 0.4 ----------- diff --git a/docs/config.rst b/docs/config.rst index 73d7c8fb..fabf6dc4 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -59,8 +59,17 @@ The following configuration values are used internally by Flask: ``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as :class:`datetime.timedelta` object. ``USE_X_SENDFILE`` enable/disable x-sendfile +``LOGGER_NAME`` the name of the logger +``SERVER_NAME`` the name of the server. Required for + subdomain support (eg: ``'localhost'``) =============================== ========================================= +.. versionadded:: 0.4 + ``LOGGER_NAME`` + +.. versionadded:: 0.5 + ``SERVER_NAME`` + Configuring from Files ---------------------- diff --git a/flask.py b/flask.py index a769692e..34e903cc 100644 --- a/flask.py +++ b/flask.py @@ -141,7 +141,8 @@ class _RequestContext(object): def __init__(self, app, environ): self.app = app - self.url_adapter = app.url_map.bind_to_environ(environ) + self.url_adapter = app.url_map.bind_to_environ(environ, + server_name=app.config['SERVER_NAME']) self.request = app.request_class(environ) self.session = app.open_session(self.request) if self.session is None: @@ -889,7 +890,8 @@ class Flask(_PackageBoundObject): 'SESSION_COOKIE_NAME': 'session', 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), 'USE_X_SENDFILE': False, - 'LOGGER_NAME': None + 'LOGGER_NAME': None, + 'SERVER_NAME': None }) def __init__(self, import_name): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index f0f15a7b..cbf2711f 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -836,6 +836,26 @@ class ConfigTestCase(unittest.TestCase): os.environ = env +class SubdomainTestCase(unittest.TestCase): + + def test_basic_support(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'localhost' + @app.route('/') + def normal_index(): + return 'normal index' + @app.route('/', subdomain='test') + def test_index(): + return 'test index' + + c = app.test_client() + rv = c.get('/', 'http://localhost/') + assert rv.data == 'normal index' + + rv = c.get('/', 'http://test.localhost/') + assert rv.data == 'test index' + + def suite(): from minitwit_tests import MiniTwitTestCase from flaskr_tests import FlaskrTestCase @@ -847,6 +867,7 @@ def suite(): suite.addTest(unittest.makeSuite(SendfileTestCase)) suite.addTest(unittest.makeSuite(LoggingTestCase)) suite.addTest(unittest.makeSuite(ConfigTestCase)) + suite.addTest(unittest.makeSuite(SubdomainTestCase)) if flask.json_available: suite.addTest(unittest.makeSuite(JSONTestCase)) suite.addTest(unittest.makeSuite(MiniTwitTestCase)) From 7ea1b801cc1f7d09ee18e8d70957c9d34b870700 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 29 Jun 2010 01:32:02 +0200 Subject: [PATCH 017/207] Documented missing attributes. This fixes #71 --- flask.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/flask.py b/flask.py index 34e903cc..d2a0298c 100644 --- a/flask.py +++ b/flask.py @@ -67,7 +67,21 @@ class Request(RequestBase): :attr:`~flask.Flask.request_class` to your subclass. """ - endpoint = view_args = routing_exception = None + #: the endpoint that matched the request. This in combination with + #: :attr:`view_args` can be used to reconstruct the same or a + #: modified URL. If an exception happened when matching, this will + #: be `None`. + endpoint = None + + #: a dict of view arguments that matched the request. If an exception + #: happened when matching, this will be `None`. + view_args = None + + #: if matching the URL failed, this is the exception that will be + #: raised / was raised as part of the request handling. This is + #: usually a :exc:`~werkzeug.exceptions.NotFound` exception or + #: something similar. + routing_exception = None @property def module(self): From bcd746e8cf0fc5f405c5f32b80517a4390de6fd9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 29 Jun 2010 01:36:06 +0200 Subject: [PATCH 018/207] Added another testcase for subdomain support --- tests/flask_tests.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index cbf2711f..1c5dc729 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -855,6 +855,17 @@ class SubdomainTestCase(unittest.TestCase): rv = c.get('/', 'http://test.localhost/') assert rv.data == 'test index' + def test_subdomain_matching(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'localhost' + @app.route('/', subdomain='') + def index(user): + return 'index for %s' % user + + c = app.test_client() + rv = c.get('/', 'http://mitsuhiko.localhost/') + assert rv.data == 'index for mitsuhiko' + def suite(): from minitwit_tests import MiniTwitTestCase From 9373a71c263a63eca3c466cbc5b175ed5d4999d5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 29 Jun 2010 15:25:20 +0200 Subject: [PATCH 019/207] improved _request_ctx_stack docs --- docs/api.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index fc6f68fb..4c45c2dd 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -301,6 +301,36 @@ Useful Internals instance and can be used by extensions and application code but the use is discouraged in general. + The following attributes are always present on each layer of the + stack:: + + `app` + the active Flask application. + + `url_adapter` + the URL adapter that was used to match the request. + + `request` + the current request object. + + `session` + the active session object. + + `g` + an object with all the attributes of the :data:`flask.g` object. + + `flashes` + an internal cache for the flashed messages. + + Example usage:: + + from flask import _request_ctx_stack + + def get_session(): + ctx = _request_ctx_stack.top + if ctx is not None: + return ctx.session + .. versionchanged:: 0.4 The request context is automatically popped at the end of the request From 881aa3ab2dad11edcddd0d5996fea9bdc3132224 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 29 Jun 2010 15:59:55 +0200 Subject: [PATCH 020/207] Fixed an rst syntax error --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 4c45c2dd..b6c81d10 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -302,7 +302,7 @@ Useful Internals use is discouraged in general. The following attributes are always present on each layer of the - stack:: + stack: `app` the active Flask application. From f5d2d324b5a1cc9d7ab14ab40488d8b7f4b8c616 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 30 Jun 2010 09:49:04 +0200 Subject: [PATCH 021/207] Mention virtualenv in mod_wsgi deployment docs --- docs/deploying/mod_wsgi.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index b4753c3e..b2d25f5c 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -133,3 +133,19 @@ If your application does not run, follow this guide to troubleshoot: The reason for this is that for non-installed packages, the module filename is used to locate the resources and for symlinks the wrong filename is picked up. + +Working with Virtual Environments +--------------------------------- + +Virtual environments have the advantage that they never install the +required dependencies system wide so you have a better control over what +is used where. If you want to use a virtual environment with mod_wsgi you +have to modify your `.wsgi` file slightly. + +Add the following lines to the top of your `.wsgi` file:: + + activate_this = '/path/to/env/bin/activate_this.py' + execfile(activate_this, dict(__file__=activate_this)) + +This sets up the load paths according to the settings of the virtual +environment. Keep in mind that the path has to be absolute. From e75322206d2fd1c483494fea7bfba367cdcdd9b2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 1 Jul 2010 00:19:32 +0200 Subject: [PATCH 022/207] Fixed a broken example in the docs --- docs/quickstart.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 11b0cd84..fd9b02c2 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -628,7 +628,9 @@ unless he knows the secret key used for signing. In order to use sessions you have to set a secret key. Here is how sessions work:: - from flask import session, redirect, url_for, escape + from flask import Flask, session, redirect, url_for, escape, request + + app = Flask(__name__) @app.route('/') def index(): @@ -652,6 +654,7 @@ sessions work:: def logout(): # remove the username from the session if its there session.pop('username', None) + return redirect(url_for('index')) # set the secret key. keep this really secret: app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT' From d0357b44b02324b85af42052487eab13d0b75f81 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 1 Jul 2010 01:22:46 +0200 Subject: [PATCH 023/207] Added links to Flask-WTF and Flask-SQLAlchemy. This fixes #73 --- docs/patterns/sqlalchemy.rst | 16 +++++++++++++++- docs/patterns/wtforms.rst | 9 +++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 6a14c8e0..a66627ad 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -8,9 +8,23 @@ encouraged to use a package instead of a module for your flask application and drop the models into a separate module (:ref:`larger-applications`). While that is not necessary, it makes a lot of sense. -There are three very common ways to use SQLAlchemy. I will outline each +There are four very common ways to use SQLAlchemy. I will outline each of them here: +Flask-SQLAlchemy Extension +-------------------------- + +Because SQLAlchemy is a common database abstraction layer and object +relational mapper that requires a little bit of configuration effort, +there is a Flask extension that handles that for you. This is recommended +if you want to get started quickly. + +You can download `Flask-SQLAlchemy`_ from `PyPI +`_. + +.. _Flask-SQLAlchemy: http://packages.python.org/Flask-SQLAlchemy/ + + Declarative ----------- diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index d62c5bd3..8a113cc5 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -12,6 +12,15 @@ first. I recommend breaking up the application into multiple modules (:ref:`larger-applications`) for that and adding a separate module for the forms. +.. admonition:: Getting most of WTForms with an Extension + + The `Flask-WTF`_ extension expands on this pattern and adds a few + handful little helpers that make working with forms and Flask more + fun. You can get it from `PyPI + `_. + +.. _Flask-WTF: http://packages.python.org/Flask-WTF/ + The Forms --------- From bc662a546ed9028d93482f79314e64beae19a1d6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 1 Jul 2010 01:45:39 +0200 Subject: [PATCH 024/207] Added a section about unicode and editors. This fixes #74 --- docs/_themes | 2 +- docs/unicode.rst | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/_themes b/docs/_themes index b8e0f4f1..21cf0743 160000 --- a/docs/_themes +++ b/docs/_themes @@ -1 +1 @@ -Subproject commit b8e0f4f1bfc7c89fffb5440fcdf60edaa033c836 +Subproject commit 21cf07433147212ee6c8ab203dfa648a9239c66f diff --git a/docs/unicode.rst b/docs/unicode.rst index e9259d12..7db462b8 100644 --- a/docs/unicode.rst +++ b/docs/unicode.rst @@ -54,6 +54,8 @@ unicode. What does working with unicode in Python 2.x mean? UTF-8 for this purpose. To tell the interpreter your encoding you can put the ``# -*- coding: utf-8 -*-`` into the first or second line of your Python source file. +- Jinja is configured to decode the template files from UTF08. So make + sure to tell your editor to save the file as UTF-8 there as well. Encoding and Decoding Yourself ------------------------------ @@ -79,3 +81,27 @@ To go from unicode into a specific charset such as UTF-8 you can use the def write_file(filename, contents, charset='utf-8'): with open(filename, 'w') as f: f.write(contents.encode(charset)) + +Configuring Editors +------------------- + +Most editors save as UTF-8 by default nowadays but in case your editor is +not configured to do this you have to change it. Here some common ways to +set your editor to store as UTF-8: + +- Vim: put ``set enc=utf-8`` to your ``.vimrc`` file. + +- Emacs: either use an encoding cookie or put this into your ``.emacs`` + file:: + + (prefer-coding-system 'utf-8) + (setq default-buffer-file-coding-system 'utf-8) + +- Notepad++: + + 1. Go to *Settings -> Preferences ...* + 2. Select the "New Document/Default Directory" tab + 3. Select "UTF-8 without BOM" as encoding + + It is also recommended to use the Unix newline format, you can select + it in the same panel but this not a requirement. From 3ab318a7ddca819e13d6d161b008b62b6583453e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 1 Jul 2010 12:56:34 +0200 Subject: [PATCH 025/207] Explained Flask constructor better. This fixes #70 --- docs/quickstart.rst | 7 ++++++- flask.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index fd9b02c2..a4858f1c 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -38,7 +38,12 @@ see your hello world greeting. So what did that code do? 1. first we imported the :class:`~flask.Flask` class. An instance of this - class will be our WSGI application. + class will be our WSGI application. The first argument is the name of + the application's module. If you are using a single module (like here) + you should use `__name__` because depending on if it's started as + application or imported as module the name will be different + (``'__main__'`` versus the actual import name). For more information + on that, have a look at the :class:`~flask.Flask` documentation. 2. next we create an instance of it. We pass it the name of the module / package. This is needed so that Flask knows where it should look for templates, static files and so on. diff --git a/flask.py b/flask.py index d2a0298c..90867fcc 100644 --- a/flask.py +++ b/flask.py @@ -806,6 +806,34 @@ class Flask(_PackageBoundObject): from flask import Flask app = Flask(__name__) + + .. admonition:: About the First Parameter + + The idea of the first parameter is to give Flask an idea what + belongs to your application. This name is used to find resources + on the file system, can be used by extensions to improve debugging + information and a lot more. + + So it's important what you provide there. If you are using a single + module, `__name__` is always the correct value. If you however are + using a package, it's usually recommended to hardcode the name of + your package there. + + For example if your application is defined in `yourapplication/app.py` + you should create it with one of the two versions below:: + + app = Flask('yourapplication') + app = Flask(__name__.split('.')[0]) + + Why is that? The application will work even with `__name__`, thanks + to how resources are looked up. However it will make debugging more + painful. Certain extensions can make assumptions based on the + import name of your application. For example the Flask-SQLAlchemy + extension will look for the code in your application that triggered + an SQL query in debug mode. If the import name is not properly set + up, that debugging information is lost. (For example it would only + pick up SQL queries in `yourapplicaiton.app` and not + `yourapplication.views.frontend`) """ #: The class that is used for request objects. See :class:`~flask.Request` From a154c87cfca3e21316552b2ec558029f9c96122a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 2 Jul 2010 19:45:26 +0200 Subject: [PATCH 026/207] Documented exception catching behaviour. This fixes #75 --- flask.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/flask.py b/flask.py index 90867fcc..9c720ef0 100644 --- a/flask.py +++ b/flask.py @@ -1090,6 +1090,16 @@ class Flask(_PackageBoundObject): :attr:`debug` flag is set the server will automatically reload for code changes and show a debugger in case an exception happened. + .. admonition:: Keep in Mind + + Flask will supress any server error with a generic error page + unless it is in debug mode. As such to enable just the + interactive debugger without the code reloading, you ahve to + invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. + Setting ``use_debugger`` to `True` without being in debug mode + won't catch any exceptions because there won't be any to + catch. + :param host: the hostname to listen on. set this to ``'0.0.0.0'`` to have the server available externally as well. :param port: the port of the webserver From ee16a68bbd7caf4c8e79c5d6859f72ad38539bbf Mon Sep 17 00:00:00 2001 From: Justin Quick Date: Fri, 2 Jul 2010 14:19:26 -0400 Subject: [PATCH 027/207] out with the old --- flask.py | 1573 ------------------------------------------------------ 1 file changed, 1573 deletions(-) delete mode 100644 flask.py diff --git a/flask.py b/flask.py deleted file mode 100644 index 9c720ef0..00000000 --- a/flask.py +++ /dev/null @@ -1,1573 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask - ~~~~~ - - A microframework based on Werkzeug. It's extensively documented - and follows best practice patterns. - - :copyright: (c) 2010 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" -from __future__ import with_statement -import os -import sys -import mimetypes -from datetime import datetime, timedelta - -# this is a workaround for appengine. Do not remove this import -import werkzeug - -from itertools import chain -from threading import Lock -from jinja2 import Environment, PackageLoader, FileSystemLoader -from werkzeug import Request as RequestBase, Response as ResponseBase, \ - LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \ - ImmutableDict, cached_property, wrap_file, Headers, \ - import_string -from werkzeug.routing import Map, Rule -from werkzeug.exceptions import HTTPException, InternalServerError -from werkzeug.contrib.securecookie import SecureCookie - -# try to load the best simplejson implementation available. If JSON -# is not installed, we add a failing class. -json_available = True -try: - import simplejson as json -except ImportError: - try: - import json - except ImportError: - json_available = False - -# utilities we import from Werkzeug and Jinja2 that are unused -# in the module but are exported as public interface. -from werkzeug import abort, redirect -from jinja2 import Markup, escape - -# use pkg_resource if that works, otherwise fall back to cwd. The -# current working directory is generally not reliable with the notable -# exception of google appengine. -try: - import pkg_resources - pkg_resources.resource_stream -except (ImportError, AttributeError): - pkg_resources = None - -# a lock used for logger initialization -_logger_lock = Lock() - - -class Request(RequestBase): - """The request object used by default in flask. Remembers the - matched endpoint and view arguments. - - It is what ends up as :class:`~flask.request`. If you want to replace - the request object used you can subclass this and set - :attr:`~flask.Flask.request_class` to your subclass. - """ - - #: the endpoint that matched the request. This in combination with - #: :attr:`view_args` can be used to reconstruct the same or a - #: modified URL. If an exception happened when matching, this will - #: be `None`. - endpoint = None - - #: a dict of view arguments that matched the request. If an exception - #: happened when matching, this will be `None`. - view_args = None - - #: if matching the URL failed, this is the exception that will be - #: raised / was raised as part of the request handling. This is - #: usually a :exc:`~werkzeug.exceptions.NotFound` exception or - #: something similar. - routing_exception = None - - @property - def module(self): - """The name of the current module""" - if self.endpoint and '.' in self.endpoint: - return self.endpoint.rsplit('.', 1)[0] - - @cached_property - def json(self): - """If the mimetype is `application/json` this will contain the - parsed JSON data. - """ - if __debug__: - _assert_have_json() - if self.mimetype == 'application/json': - return json.loads(self.data) - - -class Response(ResponseBase): - """The response object that is used by default in flask. Works like the - response object from Werkzeug but is set to have a HTML mimetype by - default. Quite often you don't have to create this object yourself because - :meth:`~flask.Flask.make_response` will take care of that for you. - - If you want to replace the response object used you can subclass this and - set :attr:`~flask.Flask.response_class` to your subclass. - """ - default_mimetype = 'text/html' - - -class _RequestGlobals(object): - pass - - -class Session(SecureCookie): - """Expands the session with support for switching between permanent - and non-permanent sessions. - """ - - def _get_permanent(self): - return self.get('_permanent', False) - - def _set_permanent(self, value): - self['_permanent'] = bool(value) - - permanent = property(_get_permanent, _set_permanent) - del _get_permanent, _set_permanent - - -class _NullSession(Session): - """Class used to generate nicer error messages if sessions are not - available. Will still allow read-only access to the empty session - but fail on setting. - """ - - def _fail(self, *args, **kwargs): - raise RuntimeError('the session is unavailable because no secret ' - 'key was set. Set the secret_key on the ' - 'application to something unique and secret') - __setitem__ = __delitem__ = clear = pop = popitem = \ - update = setdefault = _fail - del _fail - - -class _RequestContext(object): - """The request context contains all request relevant information. It is - created at the beginning of the request and pushed to the - `_request_ctx_stack` and removed at the end of it. It will create the - URL adapter and request object for the WSGI environment provided. - """ - - def __init__(self, app, environ): - self.app = app - self.url_adapter = app.url_map.bind_to_environ(environ, - server_name=app.config['SERVER_NAME']) - self.request = app.request_class(environ) - self.session = app.open_session(self.request) - if self.session is None: - self.session = _NullSession() - self.g = _RequestGlobals() - self.flashes = None - - try: - self.request.endpoint, self.request.view_args = \ - self.url_adapter.match() - except HTTPException, e: - self.request.routing_exception = e - - def push(self): - """Binds the request context.""" - _request_ctx_stack.push(self) - - def pop(self): - """Pops the request context.""" - _request_ctx_stack.pop() - - def __enter__(self): - self.push() - return self - - def __exit__(self, exc_type, exc_value, tb): - # do not pop the request stack if we are in debug mode and an - # exception happened. This will allow the debugger to still - # access the request object in the interactive shell. Furthermore - # the context can be force kept alive for the test client. - if not self.request.environ.get('flask._preserve_context') and \ - (tb is None or not self.app.debug): - self.pop() - - -def url_for(endpoint, **values): - """Generates a URL to the given endpoint with the method provided. - The endpoint is relative to the active module if modules are in use. - - Here some examples: - - ==================== ======================= ============================= - Active Module Target Endpoint Target Function - ==================== ======================= ============================= - `None` ``'index'`` `index` of the application - `None` ``'.index'`` `index` of the application - ``'admin'`` ``'index'`` `index` of the `admin` module - any ``'.index'`` `index` of the application - any ``'admin.index'`` `index` of the `admin` module - ==================== ======================= ============================= - - Variable arguments that are unknown to the target endpoint are appended - to the generated URL as query arguments. - - For more information, head over to the :ref:`Quickstart `. - - :param endpoint: the endpoint of the URL (name of the function) - :param values: the variable arguments of the URL rule - :param _external: if set to `True`, an absolute URL is generated. - """ - ctx = _request_ctx_stack.top - if '.' not in endpoint: - mod = ctx.request.module - if mod is not None: - endpoint = mod + '.' + endpoint - elif endpoint.startswith('.'): - endpoint = endpoint[1:] - external = values.pop('_external', False) - return ctx.url_adapter.build(endpoint, values, force_external=external) - - -def get_template_attribute(template_name, attribute): - """Loads a macro (or variable) a template exports. This can be used to - invoke a macro from within Python code. If you for example have a - template named `_cider.html` with the following contents: - - .. sourcecode:: html+jinja - - {% macro hello(name) %}Hello {{ name }}!{% endmacro %} - - You can access this from Python code like this:: - - hello = get_template_attribute('_cider.html', 'hello') - return hello('World') - - .. versionadded:: 0.2 - - :param template_name: the name of the template - :param attribute: the name of the variable of macro to acccess - """ - return getattr(current_app.jinja_env.get_template(template_name).module, - attribute) - - -def flash(message, category='message'): - """Flashes a message to the next request. In order to remove the - flashed message from the session and to display it to the user, - the template has to call :func:`get_flashed_messages`. - - .. versionchanged: 0.3 - `category` parameter added. - - :param message: the message to be flashed. - :param category: the category for the message. The following values - are recommended: ``'message'`` for any kind of message, - ``'error'`` for errors, ``'info'`` for information - messages and ``'warning'`` for warnings. However any - kind of string can be used as category. - """ - session.setdefault('_flashes', []).append((category, message)) - - -def get_flashed_messages(with_categories=False): - """Pulls all flashed messages from the session and returns them. - Further calls in the same request to the function will return - the same messages. By default just the messages are returned, - but when `with_categories` is set to `True`, the return value will - be a list of tuples in the form ``(category, message)`` instead. - - Example usage: - - .. sourcecode:: html+jinja - - {% for category, msg in get_flashed_messages(with_categories=true) %} -

{{ msg }} - {% endfor %} - - .. versionchanged:: 0.3 - `with_categories` parameter added. - - :param with_categories: set to `True` to also receive categories. - """ - flashes = _request_ctx_stack.top.flashes - if flashes is None: - _request_ctx_stack.top.flashes = flashes = session.pop('_flashes', []) - if not with_categories: - return [x[1] for x in flashes] - return flashes - - -def jsonify(*args, **kwargs): - """Creates a :class:`~flask.Response` with the JSON representation of - the given arguments with an `application/json` mimetype. The arguments - to this function are the same as to the :class:`dict` constructor. - - Example usage:: - - @app.route('/_get_current_user') - def get_current_user(): - return jsonify(username=g.user.username, - email=g.user.email, - id=g.user.id) - - This will send a JSON response like this to the browser:: - - { - "username": "admin", - "email": "admin@localhost", - "id": 42 - } - - This requires Python 2.6 or an installed version of simplejson. For - security reasons only objects are supported toplevel. For more - information about this, have a look at :ref:`json-security`. - - .. versionadded:: 0.2 - """ - if __debug__: - _assert_have_json() - return current_app.response_class(json.dumps(dict(*args, **kwargs), - 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): - """Renders a template from the template folder with the given - context. - - :param template_name: the name of the template to be rendered - :param context: the variables that should be available in the - context of the template. - """ - current_app.update_template_context(context) - return current_app.jinja_env.get_template(template_name).render(context) - - -def render_template_string(source, **context): - """Renders a template from the given template source string - with the given context. - - :param template_name: the sourcecode of the template to be - rendered - :param context: the variables that should be available in the - context of the template. - """ - current_app.update_template_context(context) - return current_app.jinja_env.from_string(source).render(context) - - -def _default_template_ctx_processor(): - """Default template context processor. Injects `request`, - `session` and `g`. - """ - reqctx = _request_ctx_stack.top - return dict( - request=reqctx.request, - session=reqctx.session, - g=reqctx.g - ) - - -def _assert_have_json(): - """Helper function that fails if JSON is unavailable.""" - if not json_available: - raise RuntimeError('simplejson not installed') - - -def _get_package_path(name): - """Returns the path to a package or cwd if that cannot be found.""" - try: - return os.path.abspath(os.path.dirname(sys.modules[name].__file__)) - except (KeyError, AttributeError): - return os.getcwd() - - -# figure out if simplejson escapes slashes. This behaviour was changed -# from one version to another without reason. -if not json_available or '\\/' not in json.dumps('/'): - - def _tojson_filter(*args, **kwargs): - if __debug__: - _assert_have_json() - return json.dumps(*args, **kwargs).replace('/', '\\/') -else: - _tojson_filter = json.dumps - - -class _PackageBoundObject(object): - - def __init__(self, import_name): - #: The name of the package or module. Do not change this once - #: it was set by the constructor. - self.import_name = import_name - - #: Where is the app root located? - self.root_path = _get_package_path(self.import_name) - - def open_resource(self, resource): - """Opens a resource from the application's resource folder. To see - how this works, consider the following folder structure:: - - /myapplication.py - /schemal.sql - /static - /style.css - /templates - /layout.html - /index.html - - If you want to open the `schema.sql` file you would do the - following:: - - with app.open_resource('schema.sql') as f: - contents = f.read() - do_something_with(contents) - - :param resource: the name of the resource. To access resources within - subfolders use forward slashes as separator. - """ - if pkg_resources is None: - return open(os.path.join(self.root_path, resource), 'rb') - return pkg_resources.resource_stream(self.import_name, resource) - - -class _ModuleSetupState(object): - - def __init__(self, app, url_prefix=None): - self.app = app - self.url_prefix = url_prefix - - -class Module(_PackageBoundObject): - """Container object that enables pluggable applications. A module can - be used to organize larger applications. They represent blueprints that, - in combination with a :class:`Flask` object are used to create a large - application. - - A module is like an application bound to an `import_name`. Multiple - modules can share the same import names, but in that case a `name` has - to be provided to keep them apart. If different import names are used, - the rightmost part of the import name is used as name. - - Here an example structure for a larger appliation:: - - /myapplication - /__init__.py - /views - /__init__.py - /admin.py - /frontend.py - - The `myapplication/__init__.py` can look like this:: - - from flask import Flask - from myapplication.views.admin import admin - from myapplication.views.frontend import frontend - - app = Flask(__name__) - app.register_module(admin, url_prefix='/admin') - app.register_module(frontend) - - And here an example view module (`myapplication/views/admin.py`):: - - from flask import Module - - admin = Module(__name__) - - @admin.route('/') - def index(): - pass - - @admin.route('/login') - def login(): - pass - - For a gentle introduction into modules, checkout the - :ref:`working-with-modules` section. - """ - - def __init__(self, import_name, name=None, url_prefix=None): - if name is None: - assert '.' in import_name, 'name required if package name ' \ - 'does not point to a submodule' - name = import_name.rsplit('.', 1)[1] - _PackageBoundObject.__init__(self, import_name) - self.name = name - self.url_prefix = url_prefix - self._register_events = [] - - def route(self, rule, **options): - """Like :meth:`Flask.route` but for a module. The endpoint for the - :func:`url_for` function is prefixed with the name of the module. - """ - def decorator(f): - self.add_url_rule(rule, f.__name__, f, **options) - return f - return decorator - - def add_url_rule(self, rule, endpoint, view_func=None, **options): - """Like :meth:`Flask.add_url_rule` but for a module. The endpoint for - the :func:`url_for` function is prefixed with the name of the module. - """ - def register_rule(state): - the_rule = rule - if state.url_prefix: - the_rule = state.url_prefix + rule - state.app.add_url_rule(the_rule, '%s.%s' % (self.name, endpoint), - view_func, **options) - self._record(register_rule) - - def before_request(self, f): - """Like :meth:`Flask.before_request` but for a module. This function - is only executed before each request that is handled by a function of - that module. - """ - self._record(lambda s: s.app.before_request_funcs - .setdefault(self.name, []).append(f)) - return f - - def before_app_request(self, f): - """Like :meth:`Flask.before_request`. Such a function is executed - before each request, even if outside of a module. - """ - self._record(lambda s: s.app.before_request_funcs - .setdefault(None, []).append(f)) - return f - - def after_request(self, f): - """Like :meth:`Flask.after_request` but for a module. This function - is only executed after each request that is handled by a function of - that module. - """ - self._record(lambda s: s.app.after_request_funcs - .setdefault(self.name, []).append(f)) - return f - - def after_app_request(self, f): - """Like :meth:`Flask.after_request` but for a module. Such a function - is executed after each request, even if outside of the module. - """ - self._record(lambda s: s.app.after_request_funcs - .setdefault(None, []).append(f)) - return f - - def context_processor(self, f): - """Like :meth:`Flask.context_processor` but for a module. This - function is only executed for requests handled by a module. - """ - self._record(lambda s: s.app.template_context_processors - .setdefault(self.name, []).append(f)) - return f - - def app_context_processor(self, f): - """Like :meth:`Flask.context_processor` but for a module. Such a - function is executed each request, even if outside of the module. - """ - self._record(lambda s: s.app.template_context_processors - .setdefault(None, []).append(f)) - return f - - def app_errorhandler(self, code): - """Like :meth:`Flask.errorhandler` but for a module. This - handler is used for all requests, even if outside of the module. - - .. versionadded:: 0.4 - """ - def decorator(f): - self._record(lambda s: s.app.errorhandler(code)(f)) - return f - return decorator - - def _record(self, func): - self._register_events.append(func) - - -class ConfigAttribute(object): - """Makes an attribute forward to the config""" - - def __init__(self, name): - self.__name__ = name - - def __get__(self, obj, type=None): - if obj is None: - return self - return obj.config[self.__name__] - - def __set__(self, obj, value): - obj.config[self.__name__] = value - - -class Config(dict): - """Works exactly like a dict but provides ways to fill it from files - or special dictionaries. There are two common patterns to populate the - config. - - Either you can fill the config from a config file:: - - app.config.from_pyfile('yourconfig.cfg') - - Or alternatively you can define the configuration options in the - module that calls :meth:`from_object` or provide an import path to - a module that should be loaded. It is also possible to tell it to - use the same module and with that provide the configuration values - just before the call:: - - DEBUG = True - SECRET_KEY = 'development key' - app.config.from_object(__name__) - - In both cases (loading from any Python file or loading from modules), - only uppercase keys are added to the config. This makes it possible to use - lowercase values in the config file for temporary values that are not added - to the config or to define the config keys in the same file that implements - the application. - - Probably the most interesting way to load configurations is from an - environment variable pointing to a file:: - - app.config.from_envvar('YOURAPPLICATION_SETTINGS') - - In this case before launching the application you have to set this - environment variable to the file you want to use. On Linux and OS X - use the export statement:: - - export YOURAPPLICATION_SETTINGS='/path/to/config/file' - - On windows use `set` instead. - - :param root_path: path to which files are read relative from. When the - config object is created by the application, this is - the application's :attr:`~flask.Flask.root_path`. - :param defaults: an optional dictionary of default values - """ - - def __init__(self, root_path, defaults=None): - dict.__init__(self, defaults or {}) - self.root_path = root_path - - def from_envvar(self, variable_name, silent=False): - """Loads a configuration from an environment variable pointing to - a configuration file. This basically is just a shortcut with nicer - error messages for this line of code:: - - app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) - - :param variable_name: name of the environment variable - :param silent: set to `True` if you want silent failing for missing - files. - :return: bool. `True` if able to load config, `False` otherwise. - """ - rv = os.environ.get(variable_name) - if not rv: - if silent: - return False - raise RuntimeError('The environment variable %r is not set ' - 'and as such configuration could not be ' - 'loaded. Set this variable and make it ' - 'point to a configuration file' % - variable_name) - self.from_pyfile(rv) - return True - - def from_pyfile(self, filename): - """Updates the values in the config from a Python file. This function - behaves as if the file was imported as module with the - :meth:`from_object` function. - - :param filename: the filename of the config. This can either be an - absolute filename or a filename relative to the - root path. - """ - filename = os.path.join(self.root_path, filename) - d = type(sys)('config') - d.__file__ = filename - execfile(filename, d.__dict__) - self.from_object(d) - - def from_object(self, obj): - """Updates the values from the given object. An object can be of one - of the following two types: - - - a string: in this case the object with that name will be imported - - an actual object reference: that object is used directly - - Objects are usually either modules or classes. - - Just the uppercase variables in that object are stored in the config - after lowercasing. Example usage:: - - app.config.from_object('yourapplication.default_config') - from yourapplication import default_config - app.config.from_object(default_config) - - You should not use this function to load the actual configuration but - rather configuration defaults. The actual config should be loaded - with :meth:`from_pyfile` and ideally from a location not within the - package because the package might be installed system wide. - - :param obj: an import name or object - """ - if isinstance(obj, basestring): - obj = import_string(obj) - for key in dir(obj): - if key.isupper(): - self[key] = getattr(obj, key) - - def __repr__(self): - return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) - - -class Flask(_PackageBoundObject): - """The flask object implements a WSGI application and acts as the central - object. It is passed the name of the module or package of the - application. Once it is created it will act as a central registry for - the view functions, the URL rules, template configuration and much more. - - The name of the package is used to resolve resources from inside the - package or the folder the module is contained in depending on if the - package parameter resolves to an actual python package (a folder with - an `__init__.py` file inside) or a standard module (just a `.py` file). - - For more information about resource loading, see :func:`open_resource`. - - Usually you create a :class:`Flask` instance in your main module or - in the `__init__.py` file of your package like this:: - - from flask import Flask - app = Flask(__name__) - - .. admonition:: About the First Parameter - - The idea of the first parameter is to give Flask an idea what - belongs to your application. This name is used to find resources - on the file system, can be used by extensions to improve debugging - information and a lot more. - - So it's important what you provide there. If you are using a single - module, `__name__` is always the correct value. If you however are - using a package, it's usually recommended to hardcode the name of - your package there. - - For example if your application is defined in `yourapplication/app.py` - you should create it with one of the two versions below:: - - app = Flask('yourapplication') - app = Flask(__name__.split('.')[0]) - - Why is that? The application will work even with `__name__`, thanks - to how resources are looked up. However it will make debugging more - painful. Certain extensions can make assumptions based on the - import name of your application. For example the Flask-SQLAlchemy - extension will look for the code in your application that triggered - an SQL query in debug mode. If the import name is not properly set - up, that debugging information is lost. (For example it would only - pick up SQL queries in `yourapplicaiton.app` and not - `yourapplication.views.frontend`) - """ - - #: The class that is used for request objects. See :class:`~flask.Request` - #: for more information. - request_class = Request - - #: The class that is used for response objects. See - #: :class:`~flask.Response` for more information. - response_class = Response - - #: Path for the static files. If you don't want to use static files - #: you can set this value to `None` in which case no URL rule is added - #: and the development server will no longer serve any static files. - static_path = '/static' - - #: The debug flag. Set this to `True` to enable debugging of the - #: application. In debug mode the debugger will kick in when an unhandled - #: exception ocurrs and the integrated server will automatically reload - #: the application if changes in the code are detected. - #: - #: This attribute can also be configured from the config with the `DEBUG` - #: configuration key. Defaults to `False`. - debug = ConfigAttribute('DEBUG') - - #: The testing flask. Set this to `True` to enable the test mode of - #: Flask extensions (and in the future probably also Flask itself). - #: For example this might activate unittest helpers that have an - #: additional runtime cost which should not be enabled by default. - #: - #: This attribute can also be configured from the config with the - #: `TESTING` configuration key. Defaults to `False`. - testing = ConfigAttribute('TESTING') - - #: If a secret key is set, cryptographic components can use this to - #: sign cookies and other things. Set this to a complex random value - #: when you want to use the secure cookie for instance. - #: - #: This attribute can also be configured from the config with the - #: `SECRET_KEY` configuration key. Defaults to `None`. - secret_key = ConfigAttribute('SECRET_KEY') - - #: The secure cookie uses this for the name of the session cookie. - #: - #: This attribute can also be configured from the config with the - #: `SESSION_COOKIE_NAME` configuration key. Defaults to ``'session'`` - session_cookie_name = ConfigAttribute('SESSION_COOKIE_NAME') - - #: A :class:`~datetime.timedelta` which is used to set the expiration - #: date of a permanent session. The default is 31 days which makes a - #: permanent session survive for roughly one month. - #: - #: This attribute can also be configured from the config with the - #: `PERMANENT_SESSION_LIFETIME` configuration key. Defaults to - #: ``timedelta(days=31)`` - permanent_session_lifetime = ConfigAttribute('PERMANENT_SESSION_LIFETIME') - - #: 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 - #: - #: This attribute can also be configured from the config with the - #: `USE_X_SENDFILE` configuration key. Defaults to `False`. - use_x_sendfile = ConfigAttribute('USE_X_SENDFILE') - - #: The name of the logger to use. By default the logger name is the - #: package name passed to the constructor. - #: - #: .. versionadded:: 0.4 - logger_name = ConfigAttribute('LOGGER_NAME') - - #: The logging format used for the debug logger. This is only used when - #: the application is in debug mode, otherwise the attached logging - #: handler does the formatting. - #: - #: .. versionadded:: 0.3 - debug_log_format = ( - '-' * 80 + '\n' + - '%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' + - '%(message)s\n' + - '-' * 80 - ) - - #: Options that are passed directly to the Jinja2 environment. - jinja_options = ImmutableDict( - autoescape=True, - extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] - ) - - #: Default configuration parameters. - default_config = ImmutableDict({ - 'DEBUG': False, - 'TESTING': False, - 'SECRET_KEY': None, - 'SESSION_COOKIE_NAME': 'session', - 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), - 'USE_X_SENDFILE': False, - 'LOGGER_NAME': None, - 'SERVER_NAME': None - }) - - def __init__(self, import_name): - _PackageBoundObject.__init__(self, import_name) - - #: The configuration dictionary as :class:`Config`. This behaves - #: exactly like a regular dictionary but supports additional methods - #: to load a config from files. - self.config = Config(self.root_path, self.default_config) - - #: Prepare the deferred setup of the logger. - self._logger = None - self.logger_name = self.import_name - - #: A dictionary of all view functions registered. The keys will - #: be function names which are also used to generate URLs and - #: the values are the function objects themselves. - #: to register a view function, use the :meth:`route` decorator. - self.view_functions = {} - - #: A dictionary of all registered error handlers. The key is - #: be the error code as integer, the value the function that - #: should handle that error. - #: To register a error handler, use the :meth:`errorhandler` - #: decorator. - self.error_handlers = {} - - #: A dictionary with lists of functions that should be called at the - #: beginning of the request. The key of the dictionary is the name of - #: the module this function is active for, `None` for all requests. - #: This can for example be used to open database connections or - #: getting hold of the currently logged in user. To register a - #: function here, use the :meth:`before_request` decorator. - self.before_request_funcs = {} - - #: A dictionary with lists of functions that should be called after - #: each request. The key of the dictionary is the name of the module - #: this function is active for, `None` for all requests. This can for - #: example be used to open database connections or getting hold of the - #: currently logged in user. To register a function here, use the - #: :meth:`before_request` decorator. - self.after_request_funcs = {} - - #: A dictionary with list of functions that are called without argument - #: to populate the template context. They key of the dictionary is the - #: name of the module this function is active for, `None` for all - #: requests. Each returns a dictionary that the template context is - #: updated with. To register a function here, use the - #: :meth:`context_processor` decorator. - self.template_context_processors = { - None: [_default_template_ctx_processor] - } - - #: The :class:`~werkzeug.routing.Map` for this instance. You can use - #: this to change the routing converters after the class was created - #: but before any routes are connected. Example:: - #: - #: from werkzeug import BaseConverter - #: - #: class ListConverter(BaseConverter): - #: def to_python(self, value): - #: return value.split(',') - #: def to_url(self, values): - #: return ','.join(BaseConverter.to_url(value) - #: for value in values) - #: - #: app = Flask(__name__) - #: app.url_map.converters['list'] = ListConverter - self.url_map = Map() - - if self.static_path is not None: - self.add_url_rule(self.static_path + '/', - build_only=True, endpoint='static') - if pkg_resources is not None: - target = (self.import_name, 'static') - else: - target = os.path.join(self.root_path, 'static') - self.wsgi_app = SharedDataMiddleware(self.wsgi_app, { - self.static_path: target - }) - - #: The Jinja2 environment. It is created from the - #: :attr:`jinja_options` and the loader that is returned - #: by the :meth:`create_jinja_loader` function. - self.jinja_env = Environment(loader=self.create_jinja_loader(), - **self.jinja_options) - self.jinja_env.globals.update( - url_for=url_for, - get_flashed_messages=get_flashed_messages - ) - self.jinja_env.filters['tojson'] = _tojson_filter - - @property - def logger(self): - """A :class:`logging.Logger` object for this application. The - default configuration is to log to stderr if the application is - in debug mode. This logger can be used to (surprise) log messages. - Here some examples:: - - app.logger.debug('A value for debugging') - app.logger.warning('A warning ocurred (%d apples)', 42) - app.logger.error('An error occoured') - - .. versionadded:: 0.3 - """ - if self._logger and self._logger.name == self.logger_name: - return self._logger - with _logger_lock: - if self._logger and self._logger.name == self.logger_name: - return self._logger - from logging import getLogger, StreamHandler, Formatter, \ - Logger, DEBUG - class DebugLogger(Logger): - def getEffectiveLevel(x): - return DEBUG if self.debug else Logger.getEffectiveLevel(x) - class DebugHandler(StreamHandler): - def emit(x, record): - StreamHandler.emit(x, record) if self.debug else None - handler = DebugHandler() - handler.setLevel(DEBUG) - handler.setFormatter(Formatter(self.debug_log_format)) - logger = getLogger(self.logger_name) - logger.__class__ = DebugLogger - logger.addHandler(handler) - self._logger = logger - return logger - - def create_jinja_loader(self): - """Creates the Jinja loader. By default just a package loader for - the configured package is returned that looks up templates in the - `templates` folder. To add other loaders it's possible to - override this method. - """ - if pkg_resources is None: - return FileSystemLoader(os.path.join(self.root_path, 'templates')) - return PackageLoader(self.import_name) - - def update_template_context(self, context): - """Update the template context with some commonly used variables. - This injects request, session and g into the template context. - - :param context: the context as a dictionary that is updated in place - to add extra variables. - """ - funcs = self.template_context_processors[None] - mod = _request_ctx_stack.top.request.module - if mod is not None and mod in self.template_context_processors: - funcs = chain(funcs, self.template_context_processors[mod]) - for func in funcs: - context.update(func()) - - def run(self, host='127.0.0.1', port=5000, **options): - """Runs the application on a local development server. If the - :attr:`debug` flag is set the server will automatically reload - for code changes and show a debugger in case an exception happened. - - .. admonition:: Keep in Mind - - Flask will supress any server error with a generic error page - unless it is in debug mode. As such to enable just the - interactive debugger without the code reloading, you ahve to - invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. - Setting ``use_debugger`` to `True` without being in debug mode - won't catch any exceptions because there won't be any to - catch. - - :param host: the hostname to listen on. set this to ``'0.0.0.0'`` - to have the server available externally as well. - :param port: the port of the webserver - :param options: the options to be forwarded to the underlying - Werkzeug server. See :func:`werkzeug.run_simple` - for more information. - """ - from werkzeug import run_simple - if 'debug' in options: - self.debug = options.pop('debug') - options.setdefault('use_reloader', self.debug) - options.setdefault('use_debugger', self.debug) - return run_simple(host, port, self, **options) - - def test_client(self): - """Creates a test client for this application. For information - about unit testing head over to :ref:`testing`. - - The test client can be used in a `with` block to defer the closing down - of the context until the end of the `with` block. This is useful if - you want to access the context locals for testing:: - - with app.test_client() as c: - rv = c.get('/?vodka=42') - assert request.args['vodka'] == '42' - - .. versionchanged:: 0.4 - added support for `with` block usage for the client. - """ - from werkzeug import Client - class FlaskClient(Client): - preserve_context = context_preserved = False - def open(self, *args, **kwargs): - if self.context_preserved: - _request_ctx_stack.pop() - self.context_preserved = False - kwargs.setdefault('environ_overrides', {}) \ - ['flask._preserve_context'] = self.preserve_context - old = _request_ctx_stack.top - try: - return Client.open(self, *args, **kwargs) - finally: - self.context_preserved = _request_ctx_stack.top is not old - def __enter__(self): - self.preserve_context = True - return self - def __exit__(self, exc_type, exc_value, tb): - self.preserve_context = False - if self.context_preserved: - _request_ctx_stack.pop() - return FlaskClient(self, self.response_class, use_cookies=True) - - def open_session(self, request): - """Creates or opens a new session. Default implementation stores all - session data in a signed cookie. This requires that the - :attr:`secret_key` is set. - - :param request: an instance of :attr:`request_class`. - """ - key = self.secret_key - if key is not None: - return Session.load_cookie(request, self.session_cookie_name, - secret_key=key) - - def save_session(self, session, response): - """Saves the session if it needs updates. For the default - implementation, check :meth:`open_session`. - - :param session: the session to be saved (a - :class:`~werkzeug.contrib.securecookie.SecureCookie` - object) - :param response: an instance of :attr:`response_class` - """ - expires = None - if session.permanent: - expires = datetime.utcnow() + self.permanent_session_lifetime - session.save_cookie(response, self.session_cookie_name, - expires=expires, httponly=True) - - def register_module(self, module, **options): - """Registers a module with this application. The keyword argument - of this function are the same as the ones for the constructor of the - :class:`Module` class and will override the values of the module if - provided. - """ - options.setdefault('url_prefix', module.url_prefix) - state = _ModuleSetupState(self, **options) - for func in module._register_events: - func(state) - - def add_url_rule(self, rule, endpoint=None, view_func=None, **options): - """Connects a URL rule. Works exactly like the :meth:`route` - decorator. If a view_func is provided it will be registered with the - endpoint. - - Basically this example:: - - @app.route('/') - def index(): - pass - - Is equivalent to the following:: - - def index(): - pass - app.add_url_rule('/', 'index', index) - - If the view_func is not provided you will need to connect the endpoint - to a view function like so:: - - app.view_functions['index'] = index - - .. versionchanged:: 0.2 - `view_func` parameter added. - - :param rule: the URL rule as string - :param endpoint: the endpoint for the registered URL rule. Flask - itself assumes the name of the view function as - endpoint - :param view_func: the function to call when serving a request to the - provided endpoint - :param options: the options to be forwarded to the underlying - :class:`~werkzeug.routing.Rule` object - """ - if endpoint is None: - assert view_func is not None, 'expected view func if endpoint ' \ - 'is not provided.' - endpoint = view_func.__name__ - options['endpoint'] = endpoint - options.setdefault('methods', ('GET',)) - self.url_map.add(Rule(rule, **options)) - if view_func is not None: - self.view_functions[endpoint] = view_func - - def route(self, rule, **options): - """A decorator that is used to register a view function for a - given URL rule. Example:: - - @app.route('/') - def index(): - return 'Hello World' - - Variables parts in the route can be specified with angular - brackets (``/user/``). By default a variable part - in the URL accepts any string without a slash however a different - converter can be specified as well by using ````. - - Variable parts are passed to the view function as keyword - arguments. - - The following converters are possible: - - =========== =========================================== - `int` accepts integers - `float` like `int` but for floating point values - `path` like the default but also accepts slashes - =========== =========================================== - - Here some examples:: - - @app.route('/') - def index(): - pass - - @app.route('/') - def show_user(username): - pass - - @app.route('/post/') - def show_post(post_id): - pass - - An important detail to keep in mind is how Flask deals with trailing - slashes. The idea is to keep each URL unique so the following rules - apply: - - 1. If a rule ends with a slash and is requested without a slash - by the user, the user is automatically redirected to the same - page with a trailing slash attached. - 2. If a rule does not end with a trailing slash and the user request - the page with a trailing slash, a 404 not found is raised. - - This is consistent with how web servers deal with static files. This - also makes it possible to use relative link targets safely. - - The :meth:`route` decorator accepts a couple of other arguments - as well: - - :param rule: the URL rule as string - :param methods: a list of methods this rule should be limited - to (``GET``, ``POST`` etc.). By default a rule - just listens for ``GET`` (and implicitly ``HEAD``). - :param subdomain: specifies the rule for the subdoain in case - subdomain matching is in use. - :param strict_slashes: can be used to disable the strict slashes - setting for this rule. See above. - :param options: other options to be forwarded to the underlying - :class:`~werkzeug.routing.Rule` object. - """ - def decorator(f): - self.add_url_rule(rule, None, f, **options) - return f - return decorator - - def errorhandler(self, code): - """A decorator that is used to register a function give a given - error code. Example:: - - @app.errorhandler(404) - def page_not_found(error): - return 'This page does not exist', 404 - - You can also register a function as error handler without using - the :meth:`errorhandler` decorator. The following example is - equivalent to the one above:: - - def page_not_found(error): - return 'This page does not exist', 404 - app.error_handlers[404] = page_not_found - - :param code: the code as integer for the handler - """ - def decorator(f): - self.error_handlers[code] = f - return f - return decorator - - def template_filter(self, name=None): - """A decorator that is used to register custom template filter. - You can specify a name for the filter, otherwise the function - name will be used. Example:: - - @app.template_filter() - def reverse(s): - return s[::-1] - - :param name: the optional name of the filter, otherwise the - function name will be used. - """ - def decorator(f): - self.jinja_env.filters[name or f.__name__] = f - return f - return decorator - - def before_request(self, f): - """Registers a function to run before each request.""" - self.before_request_funcs.setdefault(None, []).append(f) - return f - - def after_request(self, f): - """Register a function to be run after each request.""" - self.after_request_funcs.setdefault(None, []).append(f) - return f - - def context_processor(self, f): - """Registers a template context processor function.""" - self.template_context_processors[None].append(f) - return f - - def handle_http_exception(self, e): - """Handles an HTTP exception. By default this will invoke the - registered error handlers and fall back to returning the - exception as response. - - .. versionadded: 0.3 - """ - handler = self.error_handlers.get(e.code) - if handler is None: - return e - return handler(e) - - def handle_exception(self, e): - """Default exception handling that kicks in when an exception - occours that is not catched. In debug mode the exception will - be re-raised immediately, otherwise it is logged and the handler - for a 500 internal server error is used. If no such handler - exists, a default 500 internal server error message is displayed. - - .. versionadded: 0.3 - """ - handler = self.error_handlers.get(500) - if self.debug: - raise - self.logger.exception('Exception on %s [%s]' % ( - request.path, - request.method - )) - if handler is None: - return InternalServerError() - return handler(e) - - def dispatch_request(self): - """Does the request dispatching. Matches the URL and returns the - return value of the view or error handler. This does not have to - be a response object. In order to convert the return value to a - proper response object, call :func:`make_response`. - """ - req = _request_ctx_stack.top.request - try: - if req.routing_exception is not None: - raise req.routing_exception - return self.view_functions[req.endpoint](**req.view_args) - except HTTPException, e: - return self.handle_http_exception(e) - - def make_response(self, rv): - """Converts the return value from a view function to a real - response object that is an instance of :attr:`response_class`. - - The following types are allowed for `rv`: - - .. tabularcolumns:: |p{3.5cm}|p{9.5cm}| - - ======================= =========================================== - :attr:`response_class` the object is returned unchanged - :class:`str` a response object is created with the - string as body - :class:`unicode` a response object is created with the - string encoded to utf-8 as body - :class:`tuple` the response object is created with the - contents of the tuple as arguments - a WSGI function the function is called as WSGI application - and buffered as response object - ======================= =========================================== - - :param rv: the return value from the view function - """ - if rv is None: - raise ValueError('View function did not return a response') - if isinstance(rv, self.response_class): - return rv - if isinstance(rv, basestring): - return self.response_class(rv) - if isinstance(rv, tuple): - return self.response_class(*rv) - return self.response_class.force_type(rv, request.environ) - - def preprocess_request(self): - """Called before the actual request dispatching and will - call every as :meth:`before_request` decorated function. - If any of these function returns a value it's handled as - if it was the return value from the view and further - request handling is stopped. - """ - funcs = self.before_request_funcs.get(None, ()) - mod = request.module - if mod and mod in self.before_request_funcs: - funcs = chain(funcs, self.before_request_funcs[mod]) - for func in funcs: - rv = func() - if rv is not None: - return rv - - def process_response(self, response): - """Can be overridden in order to modify the response object - before it's sent to the WSGI server. By default this will - call all the :meth:`after_request` decorated functions. - - :param response: a :attr:`response_class` object. - :return: a new response object or the same, has to be an - instance of :attr:`response_class`. - """ - ctx = _request_ctx_stack.top - mod = ctx.request.module - if not isinstance(ctx.session, _NullSession): - self.save_session(ctx.session, response) - funcs = () - if mod and mod in self.after_request_funcs: - funcs = chain(funcs, self.after_request_funcs[mod]) - if None in self.after_request_funcs: - funcs = chain(funcs, self.after_request_funcs[None]) - for handler in funcs: - response = handler(response) - return response - - def wsgi_app(self, environ, start_response): - """The actual WSGI application. This is not implemented in - `__call__` so that middlewares can be applied without losing a - reference to the class. So instead of doing this:: - - app = MyMiddleware(app) - - It's a better idea to do this instead:: - - app.wsgi_app = MyMiddleware(app.wsgi_app) - - Then you still have the original application object around and - can continue to call methods on it. - - .. versionchanged:: 0.4 - The :meth:`after_request` functions are now called even if an - error handler took over request processing. This ensures that - even if an exception happens database have the chance to - properly close the connection. - - :param environ: a WSGI environment - :param start_response: a callable accepting a status code, - a list of headers and an optional - exception context to start the response - """ - with self.request_context(environ): - try: - rv = self.preprocess_request() - if rv is None: - rv = self.dispatch_request() - response = self.make_response(rv) - except Exception, e: - response = self.make_response(self.handle_exception(e)) - try: - response = self.process_response(response) - except Exception, e: - response = self.make_response(self.handle_exception(e)) - return response(environ, start_response) - - def request_context(self, environ): - """Creates a request context from the given environment and binds - it to the current context. This must be used in combination with - the `with` statement because the request is only bound to the - current context for the duration of the `with` block. - - Example usage:: - - with app.request_context(environ): - do_something_with(request) - - The object returned can also be used without the `with` statement - which is useful for working in the shell. The example above is - doing exactly the same as this code:: - - ctx = app.request_context(environ) - ctx.push() - try: - do_something_with(request) - finally: - ctx.pop() - - The big advantage of this approach is that you can use it without - the try/finally statement in a shell for interactive testing: - - >>> ctx = app.test_request_context() - >>> ctx.bind() - >>> request.path - u'/' - >>> ctx.unbind() - - .. versionchanged:: 0.3 - Added support for non-with statement usage and `with` statement - is now passed the ctx object. - - :param environ: a WSGI environment - """ - return _RequestContext(self, environ) - - def test_request_context(self, *args, **kwargs): - """Creates a WSGI environment from the given values (see - :func:`werkzeug.create_environ` for more information, this - function accepts the same arguments). - """ - return self.request_context(create_environ(*args, **kwargs)) - - def __call__(self, environ, start_response): - """Shortcut for :attr:`wsgi_app`.""" - return self.wsgi_app(environ, start_response) - - -# context locals -_request_ctx_stack = LocalStack() -current_app = LocalProxy(lambda: _request_ctx_stack.top.app) -request = LocalProxy(lambda: _request_ctx_stack.top.request) -session = LocalProxy(lambda: _request_ctx_stack.top.session) -g = LocalProxy(lambda: _request_ctx_stack.top.g) From d0dc89ea802130e8a3a16b4fba73fa10815c09fb Mon Sep 17 00:00:00 2001 From: Justin Quick Date: Fri, 2 Jul 2010 14:20:58 -0400 Subject: [PATCH 028/207] in with the new. i have the bits in places where i think they should be, now i just need to work on the import scheme layout --- flask/__init__.py | 58 ++++ flask/app.py | 778 ++++++++++++++++++++++++++++++++++++++++++++++ flask/conf.py | 135 ++++++++ flask/ctx.py | 57 ++++ flask/helpers.py | 310 ++++++++++++++++++ flask/module.py | 151 +++++++++ flask/session.py | 28 ++ flask/wrappers.py | 57 ++++ 8 files changed, 1574 insertions(+) create mode 100644 flask/__init__.py create mode 100644 flask/app.py create mode 100644 flask/conf.py create mode 100644 flask/ctx.py create mode 100644 flask/helpers.py create mode 100644 flask/module.py create mode 100644 flask/session.py create mode 100644 flask/wrappers.py diff --git a/flask/__init__.py b/flask/__init__.py new file mode 100644 index 00000000..f7ae2551 --- /dev/null +++ b/flask/__init__.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +""" + flask + ~~~~~ + + A microframework based on Werkzeug. It's extensively documented + and follows best practice patterns. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from __future__ import with_statement +import os +import sys +import mimetypes +from datetime import datetime, timedelta + +# this is a workaround for appengine. Do not remove this import +import werkzeug + +from itertools import chain +from threading import Lock +from jinja2 import Environment, PackageLoader, FileSystemLoader +from werkzeug import Request as RequestBase, Response as ResponseBase, \ + LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \ + ImmutableDict, cached_property, wrap_file, Headers, \ + import_string +from werkzeug.routing import Map, Rule +from werkzeug.exceptions import HTTPException, InternalServerError +from werkzeug.contrib.securecookie import SecureCookie + + + +# utilities we import from Werkzeug and Jinja2 that are unused +# in the module but are exported as public interface. +from werkzeug import abort, redirect +from jinja2 import Markup, escape + +# use pkg_resource if that works, otherwise fall back to cwd. The +# current working directory is generally not reliable with the notable +# exception of google appengine. +try: + import pkg_resources + pkg_resources.resource_stream +except (ImportError, AttributeError): + pkg_resources = None + +# a lock used for logger initialization +_logger_lock = Lock() + + + +# context locals +_request_ctx_stack = LocalStack() +current_app = LocalProxy(lambda: _request_ctx_stack.top.app) +request = LocalProxy(lambda: _request_ctx_stack.top.request) +session = LocalProxy(lambda: _request_ctx_stack.top.session) +g = LocalProxy(lambda: _request_ctx_stack.top.g) diff --git a/flask/app.py b/flask/app.py new file mode 100644 index 00000000..f3e1f73c --- /dev/null +++ b/flask/app.py @@ -0,0 +1,778 @@ + + +class Flask(_PackageBoundObject): + """The flask object implements a WSGI application and acts as the central + object. It is passed the name of the module or package of the + application. Once it is created it will act as a central registry for + the view functions, the URL rules, template configuration and much more. + + The name of the package is used to resolve resources from inside the + package or the folder the module is contained in depending on if the + package parameter resolves to an actual python package (a folder with + an `__init__.py` file inside) or a standard module (just a `.py` file). + + For more information about resource loading, see :func:`open_resource`. + + Usually you create a :class:`Flask` instance in your main module or + in the `__init__.py` file of your package like this:: + + from flask import Flask + app = Flask(__name__) + + .. admonition:: About the First Parameter + + The idea of the first parameter is to give Flask an idea what + belongs to your application. This name is used to find resources + on the file system, can be used by extensions to improve debugging + information and a lot more. + + So it's important what you provide there. If you are using a single + module, `__name__` is always the correct value. If you however are + using a package, it's usually recommended to hardcode the name of + your package there. + + For example if your application is defined in `yourapplication/app.py` + you should create it with one of the two versions below:: + + app = Flask('yourapplication') + app = Flask(__name__.split('.')[0]) + + Why is that? The application will work even with `__name__`, thanks + to how resources are looked up. However it will make debugging more + painful. Certain extensions can make assumptions based on the + import name of your application. For example the Flask-SQLAlchemy + extension will look for the code in your application that triggered + an SQL query in debug mode. If the import name is not properly set + up, that debugging information is lost. (For example it would only + pick up SQL queries in `yourapplicaiton.app` and not + `yourapplication.views.frontend`) + """ + + #: The class that is used for request objects. See :class:`~flask.Request` + #: for more information. + request_class = Request + + #: The class that is used for response objects. See + #: :class:`~flask.Response` for more information. + response_class = Response + + #: Path for the static files. If you don't want to use static files + #: you can set this value to `None` in which case no URL rule is added + #: and the development server will no longer serve any static files. + static_path = '/static' + + #: The debug flag. Set this to `True` to enable debugging of the + #: application. In debug mode the debugger will kick in when an unhandled + #: exception ocurrs and the integrated server will automatically reload + #: the application if changes in the code are detected. + #: + #: This attribute can also be configured from the config with the `DEBUG` + #: configuration key. Defaults to `False`. + debug = ConfigAttribute('DEBUG') + + #: The testing flask. Set this to `True` to enable the test mode of + #: Flask extensions (and in the future probably also Flask itself). + #: For example this might activate unittest helpers that have an + #: additional runtime cost which should not be enabled by default. + #: + #: This attribute can also be configured from the config with the + #: `TESTING` configuration key. Defaults to `False`. + testing = ConfigAttribute('TESTING') + + #: If a secret key is set, cryptographic components can use this to + #: sign cookies and other things. Set this to a complex random value + #: when you want to use the secure cookie for instance. + #: + #: This attribute can also be configured from the config with the + #: `SECRET_KEY` configuration key. Defaults to `None`. + secret_key = ConfigAttribute('SECRET_KEY') + + #: The secure cookie uses this for the name of the session cookie. + #: + #: This attribute can also be configured from the config with the + #: `SESSION_COOKIE_NAME` configuration key. Defaults to ``'session'`` + session_cookie_name = ConfigAttribute('SESSION_COOKIE_NAME') + + #: A :class:`~datetime.timedelta` which is used to set the expiration + #: date of a permanent session. The default is 31 days which makes a + #: permanent session survive for roughly one month. + #: + #: This attribute can also be configured from the config with the + #: `PERMANENT_SESSION_LIFETIME` configuration key. Defaults to + #: ``timedelta(days=31)`` + permanent_session_lifetime = ConfigAttribute('PERMANENT_SESSION_LIFETIME') + + #: 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 + #: + #: This attribute can also be configured from the config with the + #: `USE_X_SENDFILE` configuration key. Defaults to `False`. + use_x_sendfile = ConfigAttribute('USE_X_SENDFILE') + + #: The name of the logger to use. By default the logger name is the + #: package name passed to the constructor. + #: + #: .. versionadded:: 0.4 + logger_name = ConfigAttribute('LOGGER_NAME') + + #: The logging format used for the debug logger. This is only used when + #: the application is in debug mode, otherwise the attached logging + #: handler does the formatting. + #: + #: .. versionadded:: 0.3 + debug_log_format = ( + '-' * 80 + '\n' + + '%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' + + '%(message)s\n' + + '-' * 80 + ) + + #: Options that are passed directly to the Jinja2 environment. + jinja_options = ImmutableDict( + autoescape=True, + extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] + ) + + #: Default configuration parameters. + default_config = ImmutableDict({ + 'DEBUG': False, + 'TESTING': False, + 'SECRET_KEY': None, + 'SESSION_COOKIE_NAME': 'session', + 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), + 'USE_X_SENDFILE': False, + 'LOGGER_NAME': None, + 'SERVER_NAME': None + }) + + def __init__(self, import_name): + _PackageBoundObject.__init__(self, import_name) + + #: The configuration dictionary as :class:`Config`. This behaves + #: exactly like a regular dictionary but supports additional methods + #: to load a config from files. + self.config = Config(self.root_path, self.default_config) + + #: Prepare the deferred setup of the logger. + self._logger = None + self.logger_name = self.import_name + + #: A dictionary of all view functions registered. The keys will + #: be function names which are also used to generate URLs and + #: the values are the function objects themselves. + #: to register a view function, use the :meth:`route` decorator. + self.view_functions = {} + + #: A dictionary of all registered error handlers. The key is + #: be the error code as integer, the value the function that + #: should handle that error. + #: To register a error handler, use the :meth:`errorhandler` + #: decorator. + self.error_handlers = {} + + #: A dictionary with lists of functions that should be called at the + #: beginning of the request. The key of the dictionary is the name of + #: the module this function is active for, `None` for all requests. + #: This can for example be used to open database connections or + #: getting hold of the currently logged in user. To register a + #: function here, use the :meth:`before_request` decorator. + self.before_request_funcs = {} + + #: A dictionary with lists of functions that should be called after + #: each request. The key of the dictionary is the name of the module + #: this function is active for, `None` for all requests. This can for + #: example be used to open database connections or getting hold of the + #: currently logged in user. To register a function here, use the + #: :meth:`before_request` decorator. + self.after_request_funcs = {} + + #: A dictionary with list of functions that are called without argument + #: to populate the template context. They key of the dictionary is the + #: name of the module this function is active for, `None` for all + #: requests. Each returns a dictionary that the template context is + #: updated with. To register a function here, use the + #: :meth:`context_processor` decorator. + self.template_context_processors = { + None: [_default_template_ctx_processor] + } + + #: The :class:`~werkzeug.routing.Map` for this instance. You can use + #: this to change the routing converters after the class was created + #: but before any routes are connected. Example:: + #: + #: from werkzeug import BaseConverter + #: + #: class ListConverter(BaseConverter): + #: def to_python(self, value): + #: return value.split(',') + #: def to_url(self, values): + #: return ','.join(BaseConverter.to_url(value) + #: for value in values) + #: + #: app = Flask(__name__) + #: app.url_map.converters['list'] = ListConverter + self.url_map = Map() + + if self.static_path is not None: + self.add_url_rule(self.static_path + '/', + build_only=True, endpoint='static') + if pkg_resources is not None: + target = (self.import_name, 'static') + else: + target = os.path.join(self.root_path, 'static') + self.wsgi_app = SharedDataMiddleware(self.wsgi_app, { + self.static_path: target + }) + + #: The Jinja2 environment. It is created from the + #: :attr:`jinja_options` and the loader that is returned + #: by the :meth:`create_jinja_loader` function. + self.jinja_env = Environment(loader=self.create_jinja_loader(), + **self.jinja_options) + self.jinja_env.globals.update( + url_for=url_for, + get_flashed_messages=get_flashed_messages + ) + self.jinja_env.filters['tojson'] = _tojson_filter + + @property + def logger(self): + """A :class:`logging.Logger` object for this application. The + default configuration is to log to stderr if the application is + in debug mode. This logger can be used to (surprise) log messages. + Here some examples:: + + app.logger.debug('A value for debugging') + app.logger.warning('A warning ocurred (%d apples)', 42) + app.logger.error('An error occoured') + + .. versionadded:: 0.3 + """ + if self._logger and self._logger.name == self.logger_name: + return self._logger + with _logger_lock: + if self._logger and self._logger.name == self.logger_name: + return self._logger + from logging import getLogger, StreamHandler, Formatter, \ + Logger, DEBUG + class DebugLogger(Logger): + def getEffectiveLevel(x): + return DEBUG if self.debug else Logger.getEffectiveLevel(x) + class DebugHandler(StreamHandler): + def emit(x, record): + StreamHandler.emit(x, record) if self.debug else None + handler = DebugHandler() + handler.setLevel(DEBUG) + handler.setFormatter(Formatter(self.debug_log_format)) + logger = getLogger(self.logger_name) + logger.__class__ = DebugLogger + logger.addHandler(handler) + self._logger = logger + return logger + + def create_jinja_loader(self): + """Creates the Jinja loader. By default just a package loader for + the configured package is returned that looks up templates in the + `templates` folder. To add other loaders it's possible to + override this method. + """ + if pkg_resources is None: + return FileSystemLoader(os.path.join(self.root_path, 'templates')) + return PackageLoader(self.import_name) + + def update_template_context(self, context): + """Update the template context with some commonly used variables. + This injects request, session and g into the template context. + + :param context: the context as a dictionary that is updated in place + to add extra variables. + """ + funcs = self.template_context_processors[None] + mod = _request_ctx_stack.top.request.module + if mod is not None and mod in self.template_context_processors: + funcs = chain(funcs, self.template_context_processors[mod]) + for func in funcs: + context.update(func()) + + def run(self, host='127.0.0.1', port=5000, **options): + """Runs the application on a local development server. If the + :attr:`debug` flag is set the server will automatically reload + for code changes and show a debugger in case an exception happened. + + .. admonition:: Keep in Mind + + Flask will supress any server error with a generic error page + unless it is in debug mode. As such to enable just the + interactive debugger without the code reloading, you ahve to + invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. + Setting ``use_debugger`` to `True` without being in debug mode + won't catch any exceptions because there won't be any to + catch. + + :param host: the hostname to listen on. set this to ``'0.0.0.0'`` + to have the server available externally as well. + :param port: the port of the webserver + :param options: the options to be forwarded to the underlying + Werkzeug server. See :func:`werkzeug.run_simple` + for more information. + """ + from werkzeug import run_simple + if 'debug' in options: + self.debug = options.pop('debug') + options.setdefault('use_reloader', self.debug) + options.setdefault('use_debugger', self.debug) + return run_simple(host, port, self, **options) + + def test_client(self): + """Creates a test client for this application. For information + about unit testing head over to :ref:`testing`. + + The test client can be used in a `with` block to defer the closing down + of the context until the end of the `with` block. This is useful if + you want to access the context locals for testing:: + + with app.test_client() as c: + rv = c.get('/?vodka=42') + assert request.args['vodka'] == '42' + + .. versionchanged:: 0.4 + added support for `with` block usage for the client. + """ + from werkzeug import Client + class FlaskClient(Client): + preserve_context = context_preserved = False + def open(self, *args, **kwargs): + if self.context_preserved: + _request_ctx_stack.pop() + self.context_preserved = False + kwargs.setdefault('environ_overrides', {}) \ + ['flask._preserve_context'] = self.preserve_context + old = _request_ctx_stack.top + try: + return Client.open(self, *args, **kwargs) + finally: + self.context_preserved = _request_ctx_stack.top is not old + def __enter__(self): + self.preserve_context = True + return self + def __exit__(self, exc_type, exc_value, tb): + self.preserve_context = False + if self.context_preserved: + _request_ctx_stack.pop() + return FlaskClient(self, self.response_class, use_cookies=True) + + def open_session(self, request): + """Creates or opens a new session. Default implementation stores all + session data in a signed cookie. This requires that the + :attr:`secret_key` is set. + + :param request: an instance of :attr:`request_class`. + """ + key = self.secret_key + if key is not None: + return Session.load_cookie(request, self.session_cookie_name, + secret_key=key) + + def save_session(self, session, response): + """Saves the session if it needs updates. For the default + implementation, check :meth:`open_session`. + + :param session: the session to be saved (a + :class:`~werkzeug.contrib.securecookie.SecureCookie` + object) + :param response: an instance of :attr:`response_class` + """ + expires = None + if session.permanent: + expires = datetime.utcnow() + self.permanent_session_lifetime + session.save_cookie(response, self.session_cookie_name, + expires=expires, httponly=True) + + def register_module(self, module, **options): + """Registers a module with this application. The keyword argument + of this function are the same as the ones for the constructor of the + :class:`Module` class and will override the values of the module if + provided. + """ + options.setdefault('url_prefix', module.url_prefix) + state = _ModuleSetupState(self, **options) + for func in module._register_events: + func(state) + + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + """Connects a URL rule. Works exactly like the :meth:`route` + decorator. If a view_func is provided it will be registered with the + endpoint. + + Basically this example:: + + @app.route('/') + def index(): + pass + + Is equivalent to the following:: + + def index(): + pass + app.add_url_rule('/', 'index', index) + + If the view_func is not provided you will need to connect the endpoint + to a view function like so:: + + app.view_functions['index'] = index + + .. versionchanged:: 0.2 + `view_func` parameter added. + + :param rule: the URL rule as string + :param endpoint: the endpoint for the registered URL rule. Flask + itself assumes the name of the view function as + endpoint + :param view_func: the function to call when serving a request to the + provided endpoint + :param options: the options to be forwarded to the underlying + :class:`~werkzeug.routing.Rule` object + """ + if endpoint is None: + assert view_func is not None, 'expected view func if endpoint ' \ + 'is not provided.' + endpoint = view_func.__name__ + options['endpoint'] = endpoint + options.setdefault('methods', ('GET',)) + self.url_map.add(Rule(rule, **options)) + if view_func is not None: + self.view_functions[endpoint] = view_func + + def route(self, rule, **options): + """A decorator that is used to register a view function for a + given URL rule. Example:: + + @app.route('/') + def index(): + return 'Hello World' + + Variables parts in the route can be specified with angular + brackets (``/user/``). By default a variable part + in the URL accepts any string without a slash however a different + converter can be specified as well by using ````. + + Variable parts are passed to the view function as keyword + arguments. + + The following converters are possible: + + =========== =========================================== + `int` accepts integers + `float` like `int` but for floating point values + `path` like the default but also accepts slashes + =========== =========================================== + + Here some examples:: + + @app.route('/') + def index(): + pass + + @app.route('/') + def show_user(username): + pass + + @app.route('/post/') + def show_post(post_id): + pass + + An important detail to keep in mind is how Flask deals with trailing + slashes. The idea is to keep each URL unique so the following rules + apply: + + 1. If a rule ends with a slash and is requested without a slash + by the user, the user is automatically redirected to the same + page with a trailing slash attached. + 2. If a rule does not end with a trailing slash and the user request + the page with a trailing slash, a 404 not found is raised. + + This is consistent with how web servers deal with static files. This + also makes it possible to use relative link targets safely. + + The :meth:`route` decorator accepts a couple of other arguments + as well: + + :param rule: the URL rule as string + :param methods: a list of methods this rule should be limited + to (``GET``, ``POST`` etc.). By default a rule + just listens for ``GET`` (and implicitly ``HEAD``). + :param subdomain: specifies the rule for the subdoain in case + subdomain matching is in use. + :param strict_slashes: can be used to disable the strict slashes + setting for this rule. See above. + :param options: other options to be forwarded to the underlying + :class:`~werkzeug.routing.Rule` object. + """ + def decorator(f): + self.add_url_rule(rule, None, f, **options) + return f + return decorator + + def errorhandler(self, code): + """A decorator that is used to register a function give a given + error code. Example:: + + @app.errorhandler(404) + def page_not_found(error): + return 'This page does not exist', 404 + + You can also register a function as error handler without using + the :meth:`errorhandler` decorator. The following example is + equivalent to the one above:: + + def page_not_found(error): + return 'This page does not exist', 404 + app.error_handlers[404] = page_not_found + + :param code: the code as integer for the handler + """ + def decorator(f): + self.error_handlers[code] = f + return f + return decorator + + def template_filter(self, name=None): + """A decorator that is used to register custom template filter. + You can specify a name for the filter, otherwise the function + name will be used. Example:: + + @app.template_filter() + def reverse(s): + return s[::-1] + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + def decorator(f): + self.jinja_env.filters[name or f.__name__] = f + return f + return decorator + + def before_request(self, f): + """Registers a function to run before each request.""" + self.before_request_funcs.setdefault(None, []).append(f) + return f + + def after_request(self, f): + """Register a function to be run after each request.""" + self.after_request_funcs.setdefault(None, []).append(f) + return f + + def context_processor(self, f): + """Registers a template context processor function.""" + self.template_context_processors[None].append(f) + return f + + def handle_http_exception(self, e): + """Handles an HTTP exception. By default this will invoke the + registered error handlers and fall back to returning the + exception as response. + + .. versionadded: 0.3 + """ + handler = self.error_handlers.get(e.code) + if handler is None: + return e + return handler(e) + + def handle_exception(self, e): + """Default exception handling that kicks in when an exception + occours that is not catched. In debug mode the exception will + be re-raised immediately, otherwise it is logged and the handler + for a 500 internal server error is used. If no such handler + exists, a default 500 internal server error message is displayed. + + .. versionadded: 0.3 + """ + handler = self.error_handlers.get(500) + if self.debug: + raise + self.logger.exception('Exception on %s [%s]' % ( + request.path, + request.method + )) + if handler is None: + return InternalServerError() + return handler(e) + + def dispatch_request(self): + """Does the request dispatching. Matches the URL and returns the + return value of the view or error handler. This does not have to + be a response object. In order to convert the return value to a + proper response object, call :func:`make_response`. + """ + req = _request_ctx_stack.top.request + try: + if req.routing_exception is not None: + raise req.routing_exception + return self.view_functions[req.endpoint](**req.view_args) + except HTTPException, e: + return self.handle_http_exception(e) + + def make_response(self, rv): + """Converts the return value from a view function to a real + response object that is an instance of :attr:`response_class`. + + The following types are allowed for `rv`: + + .. tabularcolumns:: |p{3.5cm}|p{9.5cm}| + + ======================= =========================================== + :attr:`response_class` the object is returned unchanged + :class:`str` a response object is created with the + string as body + :class:`unicode` a response object is created with the + string encoded to utf-8 as body + :class:`tuple` the response object is created with the + contents of the tuple as arguments + a WSGI function the function is called as WSGI application + and buffered as response object + ======================= =========================================== + + :param rv: the return value from the view function + """ + if rv is None: + raise ValueError('View function did not return a response') + if isinstance(rv, self.response_class): + return rv + if isinstance(rv, basestring): + return self.response_class(rv) + if isinstance(rv, tuple): + return self.response_class(*rv) + return self.response_class.force_type(rv, request.environ) + + def preprocess_request(self): + """Called before the actual request dispatching and will + call every as :meth:`before_request` decorated function. + If any of these function returns a value it's handled as + if it was the return value from the view and further + request handling is stopped. + """ + funcs = self.before_request_funcs.get(None, ()) + mod = request.module + if mod and mod in self.before_request_funcs: + funcs = chain(funcs, self.before_request_funcs[mod]) + for func in funcs: + rv = func() + if rv is not None: + return rv + + def process_response(self, response): + """Can be overridden in order to modify the response object + before it's sent to the WSGI server. By default this will + call all the :meth:`after_request` decorated functions. + + :param response: a :attr:`response_class` object. + :return: a new response object or the same, has to be an + instance of :attr:`response_class`. + """ + ctx = _request_ctx_stack.top + mod = ctx.request.module + if not isinstance(ctx.session, _NullSession): + self.save_session(ctx.session, response) + funcs = () + if mod and mod in self.after_request_funcs: + funcs = chain(funcs, self.after_request_funcs[mod]) + if None in self.after_request_funcs: + funcs = chain(funcs, self.after_request_funcs[None]) + for handler in funcs: + response = handler(response) + return response + + def wsgi_app(self, environ, start_response): + """The actual WSGI application. This is not implemented in + `__call__` so that middlewares can be applied without losing a + reference to the class. So instead of doing this:: + + app = MyMiddleware(app) + + It's a better idea to do this instead:: + + app.wsgi_app = MyMiddleware(app.wsgi_app) + + Then you still have the original application object around and + can continue to call methods on it. + + .. versionchanged:: 0.4 + The :meth:`after_request` functions are now called even if an + error handler took over request processing. This ensures that + even if an exception happens database have the chance to + properly close the connection. + + :param environ: a WSGI environment + :param start_response: a callable accepting a status code, + a list of headers and an optional + exception context to start the response + """ + with self.request_context(environ): + try: + rv = self.preprocess_request() + if rv is None: + rv = self.dispatch_request() + response = self.make_response(rv) + except Exception, e: + response = self.make_response(self.handle_exception(e)) + try: + response = self.process_response(response) + except Exception, e: + response = self.make_response(self.handle_exception(e)) + return response(environ, start_response) + + def request_context(self, environ): + """Creates a request context from the given environment and binds + it to the current context. This must be used in combination with + the `with` statement because the request is only bound to the + current context for the duration of the `with` block. + + Example usage:: + + with app.request_context(environ): + do_something_with(request) + + The object returned can also be used without the `with` statement + which is useful for working in the shell. The example above is + doing exactly the same as this code:: + + ctx = app.request_context(environ) + ctx.push() + try: + do_something_with(request) + finally: + ctx.pop() + + The big advantage of this approach is that you can use it without + the try/finally statement in a shell for interactive testing: + + >>> ctx = app.test_request_context() + >>> ctx.bind() + >>> request.path + u'/' + >>> ctx.unbind() + + .. versionchanged:: 0.3 + Added support for non-with statement usage and `with` statement + is now passed the ctx object. + + :param environ: a WSGI environment + """ + return _RequestContext(self, environ) + + def test_request_context(self, *args, **kwargs): + """Creates a WSGI environment from the given values (see + :func:`werkzeug.create_environ` for more information, this + function accepts the same arguments). + """ + return self.request_context(create_environ(*args, **kwargs)) + + def __call__(self, environ, start_response): + """Shortcut for :attr:`wsgi_app`.""" + return self.wsgi_app(environ, start_response) + diff --git a/flask/conf.py b/flask/conf.py new file mode 100644 index 00000000..e757212c --- /dev/null +++ b/flask/conf.py @@ -0,0 +1,135 @@ + + +class ConfigAttribute(object): + """Makes an attribute forward to the config""" + + def __init__(self, name): + self.__name__ = name + + def __get__(self, obj, type=None): + if obj is None: + return self + return obj.config[self.__name__] + + def __set__(self, obj, value): + obj.config[self.__name__] = value + + +class Config(dict): + """Works exactly like a dict but provides ways to fill it from files + or special dictionaries. There are two common patterns to populate the + config. + + Either you can fill the config from a config file:: + + app.config.from_pyfile('yourconfig.cfg') + + Or alternatively you can define the configuration options in the + module that calls :meth:`from_object` or provide an import path to + a module that should be loaded. It is also possible to tell it to + use the same module and with that provide the configuration values + just before the call:: + + DEBUG = True + SECRET_KEY = 'development key' + app.config.from_object(__name__) + + In both cases (loading from any Python file or loading from modules), + only uppercase keys are added to the config. This makes it possible to use + lowercase values in the config file for temporary values that are not added + to the config or to define the config keys in the same file that implements + the application. + + Probably the most interesting way to load configurations is from an + environment variable pointing to a file:: + + app.config.from_envvar('YOURAPPLICATION_SETTINGS') + + In this case before launching the application you have to set this + environment variable to the file you want to use. On Linux and OS X + use the export statement:: + + export YOURAPPLICATION_SETTINGS='/path/to/config/file' + + On windows use `set` instead. + + :param root_path: path to which files are read relative from. When the + config object is created by the application, this is + the application's :attr:`~flask.Flask.root_path`. + :param defaults: an optional dictionary of default values + """ + + def __init__(self, root_path, defaults=None): + dict.__init__(self, defaults or {}) + self.root_path = root_path + + def from_envvar(self, variable_name, silent=False): + """Loads a configuration from an environment variable pointing to + a configuration file. This basically is just a shortcut with nicer + error messages for this line of code:: + + app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) + + :param variable_name: name of the environment variable + :param silent: set to `True` if you want silent failing for missing + files. + :return: bool. `True` if able to load config, `False` otherwise. + """ + rv = os.environ.get(variable_name) + if not rv: + if silent: + return False + raise RuntimeError('The environment variable %r is not set ' + 'and as such configuration could not be ' + 'loaded. Set this variable and make it ' + 'point to a configuration file' % + variable_name) + self.from_pyfile(rv) + return True + + def from_pyfile(self, filename): + """Updates the values in the config from a Python file. This function + behaves as if the file was imported as module with the + :meth:`from_object` function. + + :param filename: the filename of the config. This can either be an + absolute filename or a filename relative to the + root path. + """ + filename = os.path.join(self.root_path, filename) + d = type(sys)('config') + d.__file__ = filename + execfile(filename, d.__dict__) + self.from_object(d) + + def from_object(self, obj): + """Updates the values from the given object. An object can be of one + of the following two types: + + - a string: in this case the object with that name will be imported + - an actual object reference: that object is used directly + + Objects are usually either modules or classes. + + Just the uppercase variables in that object are stored in the config + after lowercasing. Example usage:: + + app.config.from_object('yourapplication.default_config') + from yourapplication import default_config + app.config.from_object(default_config) + + You should not use this function to load the actual configuration but + rather configuration defaults. The actual config should be loaded + with :meth:`from_pyfile` and ideally from a location not within the + package because the package might be installed system wide. + + :param obj: an import name or object + """ + if isinstance(obj, basestring): + obj = import_string(obj) + for key in dir(obj): + if key.isupper(): + self[key] = getattr(obj, key) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) diff --git a/flask/ctx.py b/flask/ctx.py new file mode 100644 index 00000000..88f61394 --- /dev/null +++ b/flask/ctx.py @@ -0,0 +1,57 @@ + + +class _RequestContext(object): + """The request context contains all request relevant information. It is + created at the beginning of the request and pushed to the + `_request_ctx_stack` and removed at the end of it. It will create the + URL adapter and request object for the WSGI environment provided. + """ + + def __init__(self, app, environ): + self.app = app + self.url_adapter = app.url_map.bind_to_environ(environ, + server_name=app.config['SERVER_NAME']) + self.request = app.request_class(environ) + self.session = app.open_session(self.request) + if self.session is None: + self.session = _NullSession() + self.g = _RequestGlobals() + self.flashes = None + + try: + self.request.endpoint, self.request.view_args = \ + self.url_adapter.match() + except HTTPException, e: + self.request.routing_exception = e + + def push(self): + """Binds the request context.""" + _request_ctx_stack.push(self) + + def pop(self): + """Pops the request context.""" + _request_ctx_stack.pop() + + def __enter__(self): + self.push() + return self + + def __exit__(self, exc_type, exc_value, tb): + # do not pop the request stack if we are in debug mode and an + # exception happened. This will allow the debugger to still + # access the request object in the interactive shell. Furthermore + # the context can be force kept alive for the test client. + if not self.request.environ.get('flask._preserve_context') and \ + (tb is None or not self.app.debug): + self.pop() + +def _default_template_ctx_processor(): + """Default template context processor. Injects `request`, + `session` and `g`. + """ + reqctx = _request_ctx_stack.top + return dict( + request=reqctx.request, + session=reqctx.session, + g=reqctx.g + ) diff --git a/flask/helpers.py b/flask/helpers.py new file mode 100644 index 00000000..882cc544 --- /dev/null +++ b/flask/helpers.py @@ -0,0 +1,310 @@ +# try to load the best simplejson implementation available. If JSON +# is not installed, we add a failing class. +json_available = True +try: + import simplejson as json +except ImportError: + try: + import json + except ImportError: + json_available = False + +def _assert_have_json(): + """Helper function that fails if JSON is unavailable.""" + if not json_available: + raise RuntimeError('simplejson not installed') + +# figure out if simplejson escapes slashes. This behaviour was changed +# from one version to another without reason. +if not json_available or '\\/' not in json.dumps('/'): + + def _tojson_filter(*args, **kwargs): + if __debug__: + _assert_have_json() + return json.dumps(*args, **kwargs).replace('/', '\\/') +else: + _tojson_filter = json.dumps + +def jsonify(*args, **kwargs): + """Creates a :class:`~flask.Response` with the JSON representation of + the given arguments with an `application/json` mimetype. The arguments + to this function are the same as to the :class:`dict` constructor. + + Example usage:: + + @app.route('/_get_current_user') + def get_current_user(): + return jsonify(username=g.user.username, + email=g.user.email, + id=g.user.id) + + This will send a JSON response like this to the browser:: + + { + "username": "admin", + "email": "admin@localhost", + "id": 42 + } + + This requires Python 2.6 or an installed version of simplejson. For + security reasons only objects are supported toplevel. For more + information about this, have a look at :ref:`json-security`. + + .. versionadded:: 0.2 + """ + if __debug__: + _assert_have_json() + return current_app.response_class(json.dumps(dict(*args, **kwargs), + indent=None if request.is_xhr else 2), mimetype='application/json') + + + +def url_for(endpoint, **values): + """Generates a URL to the given endpoint with the method provided. + The endpoint is relative to the active module if modules are in use. + + Here some examples: + + ==================== ======================= ============================= + Active Module Target Endpoint Target Function + ==================== ======================= ============================= + `None` ``'index'`` `index` of the application + `None` ``'.index'`` `index` of the application + ``'admin'`` ``'index'`` `index` of the `admin` module + any ``'.index'`` `index` of the application + any ``'admin.index'`` `index` of the `admin` module + ==================== ======================= ============================= + + Variable arguments that are unknown to the target endpoint are appended + to the generated URL as query arguments. + + For more information, head over to the :ref:`Quickstart `. + + :param endpoint: the endpoint of the URL (name of the function) + :param values: the variable arguments of the URL rule + :param _external: if set to `True`, an absolute URL is generated. + """ + ctx = _request_ctx_stack.top + if '.' not in endpoint: + mod = ctx.request.module + if mod is not None: + endpoint = mod + '.' + endpoint + elif endpoint.startswith('.'): + endpoint = endpoint[1:] + external = values.pop('_external', False) + return ctx.url_adapter.build(endpoint, values, force_external=external) + + +def get_template_attribute(template_name, attribute): + """Loads a macro (or variable) a template exports. This can be used to + invoke a macro from within Python code. If you for example have a + template named `_cider.html` with the following contents: + + .. sourcecode:: html+jinja + + {% macro hello(name) %}Hello {{ name }}!{% endmacro %} + + You can access this from Python code like this:: + + hello = get_template_attribute('_cider.html', 'hello') + return hello('World') + + .. versionadded:: 0.2 + + :param template_name: the name of the template + :param attribute: the name of the variable of macro to acccess + """ + return getattr(current_app.jinja_env.get_template(template_name).module, + attribute) + + +def flash(message, category='message'): + """Flashes a message to the next request. In order to remove the + flashed message from the session and to display it to the user, + the template has to call :func:`get_flashed_messages`. + + .. versionchanged: 0.3 + `category` parameter added. + + :param message: the message to be flashed. + :param category: the category for the message. The following values + are recommended: ``'message'`` for any kind of message, + ``'error'`` for errors, ``'info'`` for information + messages and ``'warning'`` for warnings. However any + kind of string can be used as category. + """ + session.setdefault('_flashes', []).append((category, message)) + + +def get_flashed_messages(with_categories=False): + """Pulls all flashed messages from the session and returns them. + Further calls in the same request to the function will return + the same messages. By default just the messages are returned, + but when `with_categories` is set to `True`, the return value will + be a list of tuples in the form ``(category, message)`` instead. + + Example usage: + + .. sourcecode:: html+jinja + + {% for category, msg in get_flashed_messages(with_categories=true) %} +

{{ msg }} + {% endfor %} + + .. versionchanged:: 0.3 + `with_categories` parameter added. + + :param with_categories: set to `True` to also receive categories. + """ + flashes = _request_ctx_stack.top.flashes + if flashes is None: + _request_ctx_stack.top.flashes = flashes = session.pop('_flashes', []) + if not with_categories: + return [x[1] for x in flashes] + return flashes + + + +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): + """Renders a template from the template folder with the given + context. + + :param template_name: the name of the template to be rendered + :param context: the variables that should be available in the + context of the template. + """ + current_app.update_template_context(context) + return current_app.jinja_env.get_template(template_name).render(context) + + +def render_template_string(source, **context): + """Renders a template from the given template source string + with the given context. + + :param template_name: the sourcecode of the template to be + rendered + :param context: the variables that should be available in the + context of the template. + """ + current_app.update_template_context(context) + return current_app.jinja_env.from_string(source).render(context) + + + +def _get_package_path(name): + """Returns the path to a package or cwd if that cannot be found.""" + try: + return os.path.abspath(os.path.dirname(sys.modules[name].__file__)) + except (KeyError, AttributeError): + return os.getcwd() + + + +class _PackageBoundObject(object): + + def __init__(self, import_name): + #: The name of the package or module. Do not change this once + #: it was set by the constructor. + self.import_name = import_name + + #: Where is the app root located? + self.root_path = _get_package_path(self.import_name) + + def open_resource(self, resource): + """Opens a resource from the application's resource folder. To see + how this works, consider the following folder structure:: + + /myapplication.py + /schemal.sql + /static + /style.css + /templates + /layout.html + /index.html + + If you want to open the `schema.sql` file you would do the + following:: + + with app.open_resource('schema.sql') as f: + contents = f.read() + do_something_with(contents) + + :param resource: the name of the resource. To access resources within + subfolders use forward slashes as separator. + """ + if pkg_resources is None: + return open(os.path.join(self.root_path, resource), 'rb') + return pkg_resources.resource_stream(self.import_name, resource) + diff --git a/flask/module.py b/flask/module.py new file mode 100644 index 00000000..2d53f3a9 --- /dev/null +++ b/flask/module.py @@ -0,0 +1,151 @@ + + +class _ModuleSetupState(object): + + def __init__(self, app, url_prefix=None): + self.app = app + self.url_prefix = url_prefix + + +class Module(_PackageBoundObject): + """Container object that enables pluggable applications. A module can + be used to organize larger applications. They represent blueprints that, + in combination with a :class:`Flask` object are used to create a large + application. + + A module is like an application bound to an `import_name`. Multiple + modules can share the same import names, but in that case a `name` has + to be provided to keep them apart. If different import names are used, + the rightmost part of the import name is used as name. + + Here an example structure for a larger appliation:: + + /myapplication + /__init__.py + /views + /__init__.py + /admin.py + /frontend.py + + The `myapplication/__init__.py` can look like this:: + + from flask import Flask + from myapplication.views.admin import admin + from myapplication.views.frontend import frontend + + app = Flask(__name__) + app.register_module(admin, url_prefix='/admin') + app.register_module(frontend) + + And here an example view module (`myapplication/views/admin.py`):: + + from flask import Module + + admin = Module(__name__) + + @admin.route('/') + def index(): + pass + + @admin.route('/login') + def login(): + pass + + For a gentle introduction into modules, checkout the + :ref:`working-with-modules` section. + """ + + def __init__(self, import_name, name=None, url_prefix=None): + if name is None: + assert '.' in import_name, 'name required if package name ' \ + 'does not point to a submodule' + name = import_name.rsplit('.', 1)[1] + _PackageBoundObject.__init__(self, import_name) + self.name = name + self.url_prefix = url_prefix + self._register_events = [] + + def route(self, rule, **options): + """Like :meth:`Flask.route` but for a module. The endpoint for the + :func:`url_for` function is prefixed with the name of the module. + """ + def decorator(f): + self.add_url_rule(rule, f.__name__, f, **options) + return f + return decorator + + def add_url_rule(self, rule, endpoint, view_func=None, **options): + """Like :meth:`Flask.add_url_rule` but for a module. The endpoint for + the :func:`url_for` function is prefixed with the name of the module. + """ + def register_rule(state): + the_rule = rule + if state.url_prefix: + the_rule = state.url_prefix + rule + state.app.add_url_rule(the_rule, '%s.%s' % (self.name, endpoint), + view_func, **options) + self._record(register_rule) + + def before_request(self, f): + """Like :meth:`Flask.before_request` but for a module. This function + is only executed before each request that is handled by a function of + that module. + """ + self._record(lambda s: s.app.before_request_funcs + .setdefault(self.name, []).append(f)) + return f + + def before_app_request(self, f): + """Like :meth:`Flask.before_request`. Such a function is executed + before each request, even if outside of a module. + """ + self._record(lambda s: s.app.before_request_funcs + .setdefault(None, []).append(f)) + return f + + def after_request(self, f): + """Like :meth:`Flask.after_request` but for a module. This function + is only executed after each request that is handled by a function of + that module. + """ + self._record(lambda s: s.app.after_request_funcs + .setdefault(self.name, []).append(f)) + return f + + def after_app_request(self, f): + """Like :meth:`Flask.after_request` but for a module. Such a function + is executed after each request, even if outside of the module. + """ + self._record(lambda s: s.app.after_request_funcs + .setdefault(None, []).append(f)) + return f + + def context_processor(self, f): + """Like :meth:`Flask.context_processor` but for a module. This + function is only executed for requests handled by a module. + """ + self._record(lambda s: s.app.template_context_processors + .setdefault(self.name, []).append(f)) + return f + + def app_context_processor(self, f): + """Like :meth:`Flask.context_processor` but for a module. Such a + function is executed each request, even if outside of the module. + """ + self._record(lambda s: s.app.template_context_processors + .setdefault(None, []).append(f)) + return f + + def app_errorhandler(self, code): + """Like :meth:`Flask.errorhandler` but for a module. This + handler is used for all requests, even if outside of the module. + + .. versionadded:: 0.4 + """ + def decorator(f): + self._record(lambda s: s.app.errorhandler(code)(f)) + return f + return decorator + + def _record(self, func): + self._register_events.append(func) diff --git a/flask/session.py b/flask/session.py new file mode 100644 index 00000000..11951962 --- /dev/null +++ b/flask/session.py @@ -0,0 +1,28 @@ +class Session(SecureCookie): + """Expands the session with support for switching between permanent + and non-permanent sessions. + """ + + def _get_permanent(self): + return self.get('_permanent', False) + + def _set_permanent(self, value): + self['_permanent'] = bool(value) + + permanent = property(_get_permanent, _set_permanent) + del _get_permanent, _set_permanent + + +class _NullSession(Session): + """Class used to generate nicer error messages if sessions are not + available. Will still allow read-only access to the empty session + but fail on setting. + """ + + def _fail(self, *args, **kwargs): + raise RuntimeError('the session is unavailable because no secret ' + 'key was set. Set the secret_key on the ' + 'application to something unique and secret') + __setitem__ = __delitem__ = clear = pop = popitem = \ + update = setdefault = _fail + del _fail diff --git a/flask/wrappers.py b/flask/wrappers.py new file mode 100644 index 00000000..933b28a8 --- /dev/null +++ b/flask/wrappers.py @@ -0,0 +1,57 @@ +class Request(RequestBase): + """The request object used by default in flask. Remembers the + matched endpoint and view arguments. + + It is what ends up as :class:`~flask.request`. If you want to replace + the request object used you can subclass this and set + :attr:`~flask.Flask.request_class` to your subclass. + """ + + #: the endpoint that matched the request. This in combination with + #: :attr:`view_args` can be used to reconstruct the same or a + #: modified URL. If an exception happened when matching, this will + #: be `None`. + endpoint = None + + #: a dict of view arguments that matched the request. If an exception + #: happened when matching, this will be `None`. + view_args = None + + #: if matching the URL failed, this is the exception that will be + #: raised / was raised as part of the request handling. This is + #: usually a :exc:`~werkzeug.exceptions.NotFound` exception or + #: something similar. + routing_exception = None + + @property + def module(self): + """The name of the current module""" + if self.endpoint and '.' in self.endpoint: + return self.endpoint.rsplit('.', 1)[0] + + @cached_property + def json(self): + """If the mimetype is `application/json` this will contain the + parsed JSON data. + """ + if __debug__: + _assert_have_json() + if self.mimetype == 'application/json': + return json.loads(self.data) + + +class Response(ResponseBase): + """The response object that is used by default in flask. Works like the + response object from Werkzeug but is set to have a HTML mimetype by + default. Quite often you don't have to create this object yourself because + :meth:`~flask.Flask.make_response` will take care of that for you. + + If you want to replace the response object used you can subclass this and + set :attr:`~flask.Flask.response_class` to your subclass. + """ + default_mimetype = 'text/html' + + +class _RequestGlobals(object): + pass + From c4f64c1c475badaeef009ad46846e32a8e5c9b66 Mon Sep 17 00:00:00 2001 From: Justin Quick Date: Fri, 2 Jul 2010 15:10:32 -0400 Subject: [PATCH 029/207] working import layout for module --- flask/__init__.py | 47 ++++++----------------------------------------- flask/app.py | 24 ++++++++++++++++++++++-- flask/conf.py | 4 ++++ flask/ctx.py | 5 +++++ flask/globals.py | 8 ++++++++ flask/helpers.py | 26 ++++++++++++++++++++++---- flask/module.py | 1 + flask/session.py | 3 +++ flask/wrappers.py | 7 +++++++ 9 files changed, 78 insertions(+), 47 deletions(-) create mode 100644 flask/globals.py diff --git a/flask/__init__.py b/flask/__init__.py index f7ae2551..e20f1206 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -9,50 +9,15 @@ :copyright: (c) 2010 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement -import os -import sys -import mimetypes -from datetime import datetime, timedelta - -# this is a workaround for appengine. Do not remove this import -import werkzeug - -from itertools import chain -from threading import Lock -from jinja2 import Environment, PackageLoader, FileSystemLoader -from werkzeug import Request as RequestBase, Response as ResponseBase, \ - LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \ - ImmutableDict, cached_property, wrap_file, Headers, \ - import_string -from werkzeug.routing import Map, Rule -from werkzeug.exceptions import HTTPException, InternalServerError -from werkzeug.contrib.securecookie import SecureCookie - - # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. from werkzeug import abort, redirect from jinja2 import Markup, escape -# use pkg_resource if that works, otherwise fall back to cwd. The -# current working directory is generally not reliable with the notable -# exception of google appengine. -try: - import pkg_resources - pkg_resources.resource_stream -except (ImportError, AttributeError): - pkg_resources = None - -# a lock used for logger initialization -_logger_lock = Lock() - - - -# context locals -_request_ctx_stack = LocalStack() -current_app = LocalProxy(lambda: _request_ctx_stack.top.app) -request = LocalProxy(lambda: _request_ctx_stack.top.request) -session = LocalProxy(lambda: _request_ctx_stack.top.session) -g = LocalProxy(lambda: _request_ctx_stack.top.g) +from flask.app import Flask +from flask.helpers import url_for, jsonify, json_available, flash, send_file, \ + get_flashed_messages, render_template, render_template, render_template_string, \ + get_template_attribute +from flask.globals import current_app, g, request, session, _request_ctx_stack +from flask.module import Module diff --git a/flask/app.py b/flask/app.py index f3e1f73c..654a96f4 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1,3 +1,23 @@ +from threading import Lock +from datetime import timedelta, datetime +from itertools import chain + +from jinja2 import Environment, PackageLoader, FileSystemLoader +from werkzeug import ImmutableDict, SharedDataMiddleware, create_environ +from werkzeug.routing import Map, Rule +from werkzeug.exceptions import HTTPException, InternalServerError + +from flask.helpers import _PackageBoundObject, url_for, get_flashed_messages, \ + _tojson_filter, get_pkg_resources +from flask.wrappers import Request, Response +from flask.conf import ConfigAttribute, Config +from flask.ctx import _default_template_ctx_processor, _RequestContext +from flask.globals import _request_ctx_stack, request +from flask.session import Session, _NullSession +from flask.module import _ModuleSetupState + +# a lock used for logger initialization +_logger_lock = Lock() class Flask(_PackageBoundObject): @@ -219,7 +239,7 @@ class Flask(_PackageBoundObject): if self.static_path is not None: self.add_url_rule(self.static_path + '/', build_only=True, endpoint='static') - if pkg_resources is not None: + if get_pkg_resources() is not None: target = (self.import_name, 'static') else: target = os.path.join(self.root_path, 'static') @@ -279,7 +299,7 @@ class Flask(_PackageBoundObject): `templates` folder. To add other loaders it's possible to override this method. """ - if pkg_resources is None: + if get_pkg_resources() is None: return FileSystemLoader(os.path.join(self.root_path, 'templates')) return PackageLoader(self.import_name) diff --git a/flask/conf.py b/flask/conf.py index e757212c..90414155 100644 --- a/flask/conf.py +++ b/flask/conf.py @@ -1,3 +1,7 @@ +import os +import sys + +from werkzeug import import_string class ConfigAttribute(object): diff --git a/flask/ctx.py b/flask/ctx.py index 88f61394..f3eab16a 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -1,3 +1,8 @@ +from werkzeug.exceptions import HTTPException + +from flask.wrappers import _RequestGlobals +from flask.globals import _request_ctx_stack +from flask.session import _NullSession class _RequestContext(object): diff --git a/flask/globals.py b/flask/globals.py new file mode 100644 index 00000000..11fdf0b3 --- /dev/null +++ b/flask/globals.py @@ -0,0 +1,8 @@ +from werkzeug import LocalStack, LocalProxy + +# context locals +_request_ctx_stack = LocalStack() +current_app = LocalProxy(lambda: _request_ctx_stack.top.app) +request = LocalProxy(lambda: _request_ctx_stack.top.request) +session = LocalProxy(lambda: _request_ctx_stack.top.session) +g = LocalProxy(lambda: _request_ctx_stack.top.g) \ No newline at end of file diff --git a/flask/helpers.py b/flask/helpers.py index 882cc544..58dd2094 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -1,3 +1,7 @@ +import os +import sys +import mimetypes + # try to load the best simplejson implementation available. If JSON # is not installed, we add a failing class. json_available = True @@ -9,6 +13,12 @@ except ImportError: except ImportError: json_available = False +from werkzeug import Headers, wrap_file + +from flask.globals import session, _request_ctx_stack, current_app, request +from flask.wrappers import Response + + def _assert_have_json(): """Helper function that fails if JSON is unavailable.""" if not json_available: @@ -57,6 +67,17 @@ def jsonify(*args, **kwargs): return current_app.response_class(json.dumps(dict(*args, **kwargs), indent=None if request.is_xhr else 2), mimetype='application/json') +def get_pkg_resources(): + """Use pkg_resource if that works, otherwise fall back to cwd. The + current working directory is generally not reliable with the notable + exception of google appengine. + """ + try: + import pkg_resources + pkg_resources.resource_stream + except (ImportError, AttributeError): + return + return pkg_resources def url_for(endpoint, **values): @@ -164,7 +185,6 @@ def get_flashed_messages(with_categories=False): return flashes - 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 @@ -262,7 +282,6 @@ def render_template_string(source, **context): return current_app.jinja_env.from_string(source).render(context) - def _get_package_path(name): """Returns the path to a package or cwd if that cannot be found.""" try: @@ -271,7 +290,6 @@ def _get_package_path(name): return os.getcwd() - class _PackageBoundObject(object): def __init__(self, import_name): @@ -304,7 +322,7 @@ class _PackageBoundObject(object): :param resource: the name of the resource. To access resources within subfolders use forward slashes as separator. """ + pkg_resources = get_pkg_resources() if pkg_resources is None: return open(os.path.join(self.root_path, resource), 'rb') return pkg_resources.resource_stream(self.import_name, resource) - diff --git a/flask/module.py b/flask/module.py index 2d53f3a9..3d691316 100644 --- a/flask/module.py +++ b/flask/module.py @@ -1,3 +1,4 @@ +from flask.helpers import _PackageBoundObject class _ModuleSetupState(object): diff --git a/flask/session.py b/flask/session.py index 11951962..cd0d5d29 100644 --- a/flask/session.py +++ b/flask/session.py @@ -1,3 +1,6 @@ +from werkzeug.contrib.securecookie import SecureCookie + + class Session(SecureCookie): """Expands the session with support for switching between permanent and non-permanent sessions. diff --git a/flask/wrappers.py b/flask/wrappers.py index 933b28a8..091c708e 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -1,3 +1,9 @@ +from werkzeug import Request as RequestBase, Response as ResponseBase, \ + cached_property + +from helpers import json + + class Request(RequestBase): """The request object used by default in flask. Remembers the matched endpoint and view arguments. @@ -35,6 +41,7 @@ class Request(RequestBase): parsed JSON data. """ if __debug__: + from flask.helpers import _assert_have_json _assert_have_json() if self.mimetype == 'application/json': return json.loads(self.data) From 1a69c7d4bf4d87d00b5fb2551ea39ee7c932768b Mon Sep 17 00:00:00 2001 From: Justin Quick Date: Fri, 2 Jul 2010 15:11:02 -0400 Subject: [PATCH 030/207] look for json module in the right place. all tests now pass with the new module layout --- tests/flask_tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 1c5dc729..582f2e05 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -21,7 +21,7 @@ from contextlib import contextmanager from datetime import datetime from werkzeug import parse_date, parse_options_header from cStringIO import StringIO - +from flask.helpers import json example_path = os.path.join(os.path.dirname(__file__), '..', 'examples') sys.path.append(os.path.join(example_path, 'flaskr')) @@ -409,7 +409,7 @@ class JSONTestCase(unittest.TestCase): for url in '/kw', '/dict': rv = c.get(url) assert rv.mimetype == 'application/json' - assert flask.json.loads(rv.data) == d + assert json.loads(rv.data) == d def test_json_attr(self): app = flask.Flask(__name__) @@ -417,7 +417,7 @@ class JSONTestCase(unittest.TestCase): def add(): return unicode(flask.request.json['a'] + flask.request.json['b']) c = app.test_client() - rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), + rv = c.post('/add', data=json.dumps({'a': 1, 'b': 2}), content_type='application/json') assert rv.data == '3' From b2c7efd0fabcc0514e27d03ed30f5f8af1de1065 Mon Sep 17 00:00:00 2001 From: Marcus Fredriksson Date: Fri, 2 Jul 2010 00:26:41 +0800 Subject: [PATCH 031/207] Typo --- flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask.py b/flask.py index 9c720ef0..c2ccd6a9 100644 --- a/flask.py +++ b/flask.py @@ -1292,7 +1292,7 @@ class Flask(_PackageBoundObject): :param methods: a list of methods this rule should be limited to (``GET``, ``POST`` etc.). By default a rule just listens for ``GET`` (and implicitly ``HEAD``). - :param subdomain: specifies the rule for the subdoain in case + :param subdomain: specifies the rule for the subdomain in case subdomain matching is in use. :param strict_slashes: can be used to disable the strict slashes setting for this rule. See above. From 7599046d042d1bb3f2db6cdcd6458338802901c4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 11:16:24 +0200 Subject: [PATCH 032/207] Started working on refactoring Jinja integration --- CHANGES | 4 ++++ docs/quickstart.rst | 7 +++++++ flask.py | 42 ++++++++++++++++++++++++++++++++-------- tests/flask_tests.py | 8 ++++++++ tests/templates/mail.txt | 1 + 5 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 tests/templates/mail.txt diff --git a/CHANGES b/CHANGES index 3490f0e2..7336c377 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,10 @@ Codename to be decided, release date to be announced. - fixed a bug with subdomains that was caused by the inability to specify the server name. The server name can now be set with the `SERVER_NAME` config key. +- autoescaping is no longer active for all templates. Instead it + is only active for ``.html``, ``.htm``, ``.xml`` and ``.xhtml``. + Inside templates this behaviour can be changed with the + ``autoescape`` tag. Version 0.4 ----------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index a4858f1c..e1fdce51 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -414,6 +414,13 @@ Markup(u'<blink>hacker</blink>') >>> Markup('Marked up » HTML').striptags() u'Marked up \xbb HTML' +.. versionchanged:: 0.5 + + Autoescaping is no longer enabled for all templates. The following + extensions for templates trigger autoescaping: ``.html``, ``.htm``, + ``.xml``, ``.xhtml``. Templates loaded from string will have + autoescaping disabled. + .. [#] Unsure what that :class:`~flask.g` object is? It's something you can store information on yourself, check the documentation of that object (:class:`~flask.g`) and the :ref:`sqlite3` for more diff --git a/flask.py b/flask.py index 9c720ef0..c55838dc 100644 --- a/flask.py +++ b/flask.py @@ -788,6 +788,15 @@ class Config(dict): return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) +def _select_autoescape(filename): + """Returns `True` if autoescaping should be active for the given + template name. + """ + if filename is None: + return False + return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) + + class Flask(_PackageBoundObject): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the @@ -920,7 +929,7 @@ class Flask(_PackageBoundObject): #: Options that are passed directly to the Jinja2 environment. jinja_options = ImmutableDict( - autoescape=True, + autoescape=_select_autoescape, extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] ) @@ -1018,13 +1027,8 @@ class Flask(_PackageBoundObject): #: The Jinja2 environment. It is created from the #: :attr:`jinja_options` and the loader that is returned #: by the :meth:`create_jinja_loader` function. - self.jinja_env = Environment(loader=self.create_jinja_loader(), - **self.jinja_options) - self.jinja_env.globals.update( - url_for=url_for, - get_flashed_messages=get_flashed_messages - ) - self.jinja_env.filters['tojson'] = _tojson_filter + self.jinja_env = self.create_jinja_environment() + self.init_jinja_globals() @property def logger(self): @@ -1061,6 +1065,15 @@ class Flask(_PackageBoundObject): self._logger = logger return logger + def create_jinja_environment(self): + """Creates the Jinja2 environment based on :attr:`jinja_options` + and :meth:`create_jinja_loader`. + + .. versionadded:: 0.5 + """ + return Environment(loader=self.create_jinja_loader(), + **self.jinja_options) + def create_jinja_loader(self): """Creates the Jinja loader. By default just a package loader for the configured package is returned that looks up templates in the @@ -1071,6 +1084,19 @@ class Flask(_PackageBoundObject): return FileSystemLoader(os.path.join(self.root_path, 'templates')) return PackageLoader(self.import_name) + def init_jinja_globals(self): + """Callde directly after the environment was created to inject + some defaults (like `url_for`, `get_flashed_messages` and the + `tojson` filter. + + .. versionadded:: 0.5 + """ + self.jinja_env.globals.update( + url_for=url_for, + get_flashed_messages=get_flashed_messages + ) + self.jinja_env.filters['tojson'] = _tojson_filter + def update_template_context(self, context): """Update the template context with some commonly used variables. This injects request, session and g into the template context. diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 1c5dc729..4309f214 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -461,6 +461,14 @@ class TemplatingTestCase(unittest.TestCase): '

Hello World!' ] + def test_no_escaping(self): + app = flask.Flask(__name__) + with app.test_request_context(): + assert flask.render_template_string('{{ foo }}', + foo='') == '' + assert flask.render_template('mail.txt', foo='') \ + == ' Mail' + def test_macros(self): app = flask.Flask(__name__) with app.test_request_context(): diff --git a/tests/templates/mail.txt b/tests/templates/mail.txt new file mode 100644 index 00000000..d6cb92ea --- /dev/null +++ b/tests/templates/mail.txt @@ -0,0 +1 @@ +{{ foo}} Mail From 8798b4b7112fb984bd244c8581ab03ef4a7ba766 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 11:25:26 +0200 Subject: [PATCH 033/207] Merged in changes from master by hand --- flask/__init__.py | 2 +- flask/app.py | 42 ++++++++++++++++++++++++++++++++++-------- tests/flask_tests.py | 5 ++--- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index e20f1206..b57e7d07 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -18,6 +18,6 @@ from jinja2 import Markup, escape from flask.app import Flask from flask.helpers import url_for, jsonify, json_available, flash, send_file, \ get_flashed_messages, render_template, render_template, render_template_string, \ - get_template_attribute + get_template_attribute, json from flask.globals import current_app, g, request, session, _request_ctx_stack from flask.module import Module diff --git a/flask/app.py b/flask/app.py index 654a96f4..9fd7512b 100644 --- a/flask/app.py +++ b/flask/app.py @@ -20,6 +20,15 @@ from flask.module import _ModuleSetupState _logger_lock = Lock() +def _select_autoescape(filename): + """Returns `True` if autoescaping should be active for the given + template name. + """ + if filename is None: + return False + return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) + + class Flask(_PackageBoundObject): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the @@ -152,7 +161,7 @@ class Flask(_PackageBoundObject): #: Options that are passed directly to the Jinja2 environment. jinja_options = ImmutableDict( - autoescape=True, + autoescape=_select_autoescape, extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] ) @@ -250,13 +259,8 @@ class Flask(_PackageBoundObject): #: The Jinja2 environment. It is created from the #: :attr:`jinja_options` and the loader that is returned #: by the :meth:`create_jinja_loader` function. - self.jinja_env = Environment(loader=self.create_jinja_loader(), - **self.jinja_options) - self.jinja_env.globals.update( - url_for=url_for, - get_flashed_messages=get_flashed_messages - ) - self.jinja_env.filters['tojson'] = _tojson_filter + self.jinja_env = self.create_jinja_environment() + self.init_jinja_globals() @property def logger(self): @@ -293,6 +297,15 @@ class Flask(_PackageBoundObject): self._logger = logger return logger + def create_jinja_environment(self): + """Creates the Jinja2 environment based on :attr:`jinja_options` + and :meth:`create_jinja_loader`. + + .. versionadded:: 0.5 + """ + return Environment(loader=self.create_jinja_loader(), + **self.jinja_options) + def create_jinja_loader(self): """Creates the Jinja loader. By default just a package loader for the configured package is returned that looks up templates in the @@ -303,6 +316,19 @@ class Flask(_PackageBoundObject): return FileSystemLoader(os.path.join(self.root_path, 'templates')) return PackageLoader(self.import_name) + def init_jinja_globals(self): + """Callde directly after the environment was created to inject + some defaults (like `url_for`, `get_flashed_messages` and the + `tojson` filter. + + .. versionadded:: 0.5 + """ + self.jinja_env.globals.update( + url_for=url_for, + get_flashed_messages=get_flashed_messages + ) + self.jinja_env.filters['tojson'] = _tojson_filter + def update_template_context(self, context): """Update the template context with some commonly used variables. This injects request, session and g into the template context. diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 08194f6e..7e2c9246 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -21,7 +21,6 @@ from contextlib import contextmanager from datetime import datetime from werkzeug import parse_date, parse_options_header from cStringIO import StringIO -from flask.helpers import json example_path = os.path.join(os.path.dirname(__file__), '..', 'examples') sys.path.append(os.path.join(example_path, 'flaskr')) @@ -409,7 +408,7 @@ class JSONTestCase(unittest.TestCase): for url in '/kw', '/dict': rv = c.get(url) assert rv.mimetype == 'application/json' - assert json.loads(rv.data) == d + assert flask.json.loads(rv.data) == d def test_json_attr(self): app = flask.Flask(__name__) @@ -417,7 +416,7 @@ class JSONTestCase(unittest.TestCase): def add(): return unicode(flask.request.json['a'] + flask.request.json['b']) c = app.test_client() - rv = c.post('/add', data=json.dumps({'a': 1, 'b': 2}), + rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), content_type='application/json') assert rv.data == '3' From dd59d7241d0ebc713d51ab939f53ebd0df8b2dac Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 11:26:34 +0200 Subject: [PATCH 034/207] Ported a typo fix from master --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 9fd7512b..273e9b34 100644 --- a/flask/app.py +++ b/flask/app.py @@ -550,7 +550,7 @@ class Flask(_PackageBoundObject): :param methods: a list of methods this rule should be limited to (``GET``, ``POST`` etc.). By default a rule just listens for ``GET`` (and implicitly ``HEAD``). - :param subdomain: specifies the rule for the subdoain in case + :param subdomain: specifies the rule for the subdomain in case subdomain matching is in use. :param strict_slashes: can be used to disable the strict slashes setting for this rule. See above. From 4f8ee8f12946b224e5003405be99aa4e454bdeee Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 11:35:20 +0200 Subject: [PATCH 035/207] Added missing comments, fixed setup.py and made tests pass --- flask/__init__.py | 1 + flask/app.py | 13 ++++++++++++- flask/{conf.py => config.py} | 11 +++++++++++ flask/ctx.py | 16 +++++++++++++++- flask/globals.py | 14 +++++++++++++- flask/helpers.py | 19 ++++++++++++++++--- flask/module.py | 11 +++++++++++ flask/session.py | 12 ++++++++++++ flask/wrappers.py | 16 +++++++++++----- setup.py | 2 +- 10 files changed, 103 insertions(+), 12 deletions(-) rename flask/{conf.py => config.py} (96%) diff --git a/flask/__init__.py b/flask/__init__.py index b57e7d07..8cfe7fca 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -16,6 +16,7 @@ from werkzeug import abort, redirect from jinja2 import Markup, escape from flask.app import Flask +from flask.config import Config from flask.helpers import url_for, jsonify, json_available, flash, send_file, \ get_flashed_messages, render_template, render_template, render_template_string, \ get_template_attribute, json diff --git a/flask/app.py b/flask/app.py index 273e9b34..e7ed5bcb 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1,3 +1,14 @@ +# -*- coding: utf-8 -*- +""" + flask.app + ~~~~~~~~~ + + This module implements the central WSGI application object. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + from threading import Lock from datetime import timedelta, datetime from itertools import chain @@ -10,7 +21,7 @@ from werkzeug.exceptions import HTTPException, InternalServerError from flask.helpers import _PackageBoundObject, url_for, get_flashed_messages, \ _tojson_filter, get_pkg_resources from flask.wrappers import Request, Response -from flask.conf import ConfigAttribute, Config +from flask.config import ConfigAttribute, Config from flask.ctx import _default_template_ctx_processor, _RequestContext from flask.globals import _request_ctx_stack, request from flask.session import Session, _NullSession diff --git a/flask/conf.py b/flask/config.py similarity index 96% rename from flask/conf.py rename to flask/config.py index 90414155..7918de01 100644 --- a/flask/conf.py +++ b/flask/config.py @@ -1,3 +1,14 @@ +# -*- coding: utf-8 -*- +""" + flask.config + ~~~~~~~~~~~~ + + Implements the configuration related objects. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + import os import sys diff --git a/flask/ctx.py b/flask/ctx.py index f3eab16a..1c538ecf 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -1,10 +1,24 @@ +# -*- coding: utf-8 -*- +""" + flask.ctx + ~~~~~~~~~ + + Implements the objects required to keep the context. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + from werkzeug.exceptions import HTTPException -from flask.wrappers import _RequestGlobals from flask.globals import _request_ctx_stack from flask.session import _NullSession +class _RequestGlobals(object): + pass + + class _RequestContext(object): """The request context contains all request relevant information. It is created at the beginning of the request and pushed to the diff --git a/flask/globals.py b/flask/globals.py index 11fdf0b3..aac46555 100644 --- a/flask/globals.py +++ b/flask/globals.py @@ -1,3 +1,15 @@ +# -*- coding: utf-8 -*- +""" + flask.globals + ~~~~~~~~~~~~~ + + Defines all the global objects that are proxies to the current + active context. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + from werkzeug import LocalStack, LocalProxy # context locals @@ -5,4 +17,4 @@ _request_ctx_stack = LocalStack() current_app = LocalProxy(lambda: _request_ctx_stack.top.app) request = LocalProxy(lambda: _request_ctx_stack.top.request) session = LocalProxy(lambda: _request_ctx_stack.top.session) -g = LocalProxy(lambda: _request_ctx_stack.top.g) \ No newline at end of file +g = LocalProxy(lambda: _request_ctx_stack.top.g) diff --git a/flask/helpers.py b/flask/helpers.py index 58dd2094..5a6e3966 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -1,3 +1,14 @@ +# -*- coding: utf-8 -*- +""" + flask.helpers + ~~~~~~~~~~~~~ + + Implements various helpers. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + import os import sys import mimetypes @@ -12,13 +23,13 @@ except ImportError: import json except ImportError: json_available = False - + from werkzeug import Headers, wrap_file from flask.globals import session, _request_ctx_stack, current_app, request from flask.wrappers import Response - + def _assert_have_json(): """Helper function that fails if JSON is unavailable.""" if not json_available: @@ -35,6 +46,7 @@ if not json_available or '\\/' not in json.dumps('/'): else: _tojson_filter = json.dumps + def jsonify(*args, **kwargs): """Creates a :class:`~flask.Response` with the JSON representation of the given arguments with an `application/json` mimetype. The arguments @@ -66,7 +78,8 @@ def jsonify(*args, **kwargs): _assert_have_json() return current_app.response_class(json.dumps(dict(*args, **kwargs), indent=None if request.is_xhr else 2), mimetype='application/json') - + + def get_pkg_resources(): """Use pkg_resource if that works, otherwise fall back to cwd. The current working directory is generally not reliable with the notable diff --git a/flask/module.py b/flask/module.py index 3d691316..85898511 100644 --- a/flask/module.py +++ b/flask/module.py @@ -1,3 +1,14 @@ +# -*- coding: utf-8 -*- +""" + flask.module + ~~~~~~~~~~~~ + + Implements a class that represents module blueprints. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + from flask.helpers import _PackageBoundObject diff --git a/flask/session.py b/flask/session.py index cd0d5d29..324fc98d 100644 --- a/flask/session.py +++ b/flask/session.py @@ -1,3 +1,15 @@ +# -*- coding: utf-8 -*- +""" + flask.session + ~~~~~~~~~~~~~ + + Implements cookie based sessions based on Werkzeug's secure cookie + system. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + from werkzeug.contrib.securecookie import SecureCookie diff --git a/flask/wrappers.py b/flask/wrappers.py index 091c708e..d962a563 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -1,3 +1,14 @@ +# -*- coding: utf-8 -*- +""" + flask.wrappers + ~~~~~~~~~~~~~~ + + Implements the WSGI wrappers (request and response). + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + from werkzeug import Request as RequestBase, Response as ResponseBase, \ cached_property @@ -57,8 +68,3 @@ class Response(ResponseBase): set :attr:`~flask.Flask.response_class` to your subclass. """ default_mimetype = 'text/html' - - -class _RequestGlobals(object): - pass - diff --git a/setup.py b/setup.py index 404fad32..dd3561da 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,7 @@ setup( description='A microframework based on Werkzeug, Jinja2 ' 'and good intentions', long_description=__doc__, - py_modules=['flask'], + packages=['flask'], zip_safe=False, platforms='any', install_requires=[ From 88d9315d1955ea4ee99f53e21593a0d1d9047ed0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 11:37:39 +0200 Subject: [PATCH 036/207] We should now have the same public API as before --- CHANGES | 2 ++ flask/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 7336c377..795c2eb8 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,8 @@ Codename to be decided, release date to be announced. is only active for ``.html``, ``.htm``, ``.xml`` and ``.xhtml``. Inside templates this behaviour can be changed with the ``autoescape`` tag. +- refactored Flask internally. It now consists of more than a + single file. Version 0.4 ----------- diff --git a/flask/__init__.py b/flask/__init__.py index 8cfe7fca..85c8dd3f 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -15,7 +15,7 @@ from werkzeug import abort, redirect from jinja2 import Markup, escape -from flask.app import Flask +from flask.app import Flask, Request, Response from flask.config import Config from flask.helpers import url_for, jsonify, json_available, flash, send_file, \ get_flashed_messages, render_template, render_template, render_template_string, \ From d0c6ad7d287e543fcc941aa2b42557e06b9dc142 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 11:56:08 +0200 Subject: [PATCH 037/207] Added support for conditional responses to send_file --- CHANGES | 2 ++ flask/app.py | 2 +- flask/helpers.py | 37 +++++++++++++++++++++++++++++++++---- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 795c2eb8..1440781e 100644 --- a/CHANGES +++ b/CHANGES @@ -17,6 +17,8 @@ Codename to be decided, release date to be announced. ``autoescape`` tag. - refactored Flask internally. It now consists of more than a single file. +- :func:`flask.send_file` now emits etags and has the ability to + do conditional responses builtin. Version 0.4 ----------- diff --git a/flask/app.py b/flask/app.py index e7ed5bcb..062d4289 100644 --- a/flask/app.py +++ b/flask/app.py @@ -328,7 +328,7 @@ class Flask(_PackageBoundObject): return PackageLoader(self.import_name) def init_jinja_globals(self): - """Callde directly after the environment was created to inject + """Called directly after the environment was created to inject some defaults (like `url_for`, `get_flashed_messages` and the `tojson` filter. diff --git a/flask/helpers.py b/flask/helpers.py index 5a6e3966..a34e055e 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -12,6 +12,8 @@ import os import sys import mimetypes +from time import time +from zlib import adler32 # try to load the best simplejson implementation available. If JSON # is not installed, we add a failing class. @@ -24,7 +26,7 @@ except ImportError: except ImportError: json_available = False -from werkzeug import Headers, wrap_file +from werkzeug import Headers, wrap_file, is_resource_modified from flask.globals import session, _request_ctx_stack, current_app, request from flask.wrappers import Response @@ -199,7 +201,8 @@ def get_flashed_messages(with_categories=False): def send_file(filename_or_fp, mimetype=None, as_attachment=False, - attachment_filename=None): + attachment_filename=None, add_etags=True, + cache_timeout=60 * 60 * 12, conditional=False): """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 @@ -220,6 +223,10 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, .. versionadded:: 0.2 + .. versionadded:: 0.5 + The `add_etags`, `cache_timeout` and `conditional` parameters were added. + The default behaviour is now to attach etags. + :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. @@ -232,6 +239,9 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, a ``Content-Disposition: attachment`` header. :param attachment_filename: the filename for the attachment if it differs from the file's filename. + :param add_etags: set to `False` to disable attaching of etags. + :param conditional: set to `True` to enable conditional responses. + :param cache_timeout: the timeout in seconds for the headers. """ if isinstance(filename_or_fp, basestring): filename = filename_or_fp @@ -266,8 +276,27 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, file = open(filename, 'rb') data = wrap_file(request.environ, file) - return Response(data, mimetype=mimetype, headers=headers, - direct_passthrough=True) + rv = Response(data, mimetype=mimetype, headers=headers, + direct_passthrough=True) + + rv.cache_control.public = True + if cache_timeout: + rv.cache_control.max_age = cache_timeout + rv.expires = int(time() + cache_timeout) + + if add_etags and filename is not None: + rv.set_etag('flask-%s-%s-%s' % ( + os.path.getmtime(filename), + os.path.getsize(filename), + adler32(filename) & 0xffffffff + )) + if conditional: + rv = rv.make_conditional(request) + # make sure we don't send x-sendfile for servers that + # ignore the 304 status code for x-sendfile. + if rv.status_code == 304: + rv.headers.pop('x-sendfile', None) + return rv def render_template(template_name, **context): From 532347d6adf1da64259f1af91860d5fa27bac9f1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 12:06:29 +0200 Subject: [PATCH 038/207] phased out shared data middleware in flask for send_file --- flask/app.py | 32 +++++++++++++++++++++----------- flask/helpers.py | 3 ++- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/flask/app.py b/flask/app.py index 062d4289..7230950c 100644 --- a/flask/app.py +++ b/flask/app.py @@ -9,17 +9,19 @@ :license: BSD, see LICENSE for more details. """ +import os +import posixpath from threading import Lock from datetime import timedelta, datetime from itertools import chain from jinja2 import Environment, PackageLoader, FileSystemLoader -from werkzeug import ImmutableDict, SharedDataMiddleware, create_environ +from werkzeug import ImmutableDict, create_environ from werkzeug.routing import Map, Rule -from werkzeug.exceptions import HTTPException, InternalServerError +from werkzeug.exceptions import HTTPException, InternalServerError, NotFound from flask.helpers import _PackageBoundObject, url_for, get_flashed_messages, \ - _tojson_filter, get_pkg_resources + _tojson_filter, get_pkg_resources, send_file from flask.wrappers import Request, Response from flask.config import ConfigAttribute, Config from flask.ctx import _default_template_ctx_processor, _RequestContext @@ -258,14 +260,8 @@ class Flask(_PackageBoundObject): if self.static_path is not None: self.add_url_rule(self.static_path + '/', - build_only=True, endpoint='static') - if get_pkg_resources() is not None: - target = (self.import_name, 'static') - else: - target = os.path.join(self.root_path, 'static') - self.wsgi_app = SharedDataMiddleware(self.wsgi_app, { - self.static_path: target - }) + endpoint='static', + view_func=self.send_static_file) #: The Jinja2 environment. It is created from the #: :attr:`jinja_options` and the loader that is returned @@ -383,6 +379,20 @@ class Flask(_PackageBoundObject): options.setdefault('use_debugger', self.debug) return run_simple(host, port, self, **options) + def send_static_file(self, filename): + """Function used internally to send static files from the static + folder to the browser. + + .. versionadded:: 0.5 + """ + filename = posixpath.normpath(filename) + if filename.startswith('../'): + raise NotFound() + filename = os.path.join(self.root_path, 'static', filename) + if not os.path.isfile(filename): + raise NotFound() + return send_file(filename, conditional=True) + def test_client(self): """Creates a test client for this application. For information about unit testing head over to :ref:`testing`. diff --git a/flask/helpers.py b/flask/helpers.py index a34e055e..ce206eea 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -250,7 +250,8 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, file = filename_or_fp filename = getattr(file, 'name', None) if filename is not None: - filename = os.path.join(current_app.root_path, filename) + if not os.path.isabs(filename): + 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: From fedc06c2950a4c9930082a014d0f8da6ee0193be Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 12:14:00 +0200 Subject: [PATCH 039/207] dropped pkg_resources --- CHANGES | 2 ++ flask/app.py | 6 ++---- flask/helpers.py | 18 +----------------- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/CHANGES b/CHANGES index 1440781e..34cd23d8 100644 --- a/CHANGES +++ b/CHANGES @@ -19,6 +19,8 @@ Codename to be decided, release date to be announced. single file. - :func:`flask.send_file` now emits etags and has the ability to do conditional responses builtin. +- (temporarily) dropped support for zipped applications. This was a + rarely used feature and led to some confusing behaviour. Version 0.4 ----------- diff --git a/flask/app.py b/flask/app.py index 7230950c..c902f276 100644 --- a/flask/app.py +++ b/flask/app.py @@ -21,7 +21,7 @@ from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, NotFound from flask.helpers import _PackageBoundObject, url_for, get_flashed_messages, \ - _tojson_filter, get_pkg_resources, send_file + _tojson_filter, send_file from flask.wrappers import Request, Response from flask.config import ConfigAttribute, Config from flask.ctx import _default_template_ctx_processor, _RequestContext @@ -319,9 +319,7 @@ class Flask(_PackageBoundObject): `templates` folder. To add other loaders it's possible to override this method. """ - if get_pkg_resources() is None: - return FileSystemLoader(os.path.join(self.root_path, 'templates')) - return PackageLoader(self.import_name) + return FileSystemLoader(os.path.join(self.root_path, 'templates')) def init_jinja_globals(self): """Called directly after the environment was created to inject diff --git a/flask/helpers.py b/flask/helpers.py index ce206eea..0afd4a85 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -82,19 +82,6 @@ def jsonify(*args, **kwargs): indent=None if request.is_xhr else 2), mimetype='application/json') -def get_pkg_resources(): - """Use pkg_resource if that works, otherwise fall back to cwd. The - current working directory is generally not reliable with the notable - exception of google appengine. - """ - try: - import pkg_resources - pkg_resources.resource_stream - except (ImportError, AttributeError): - return - return pkg_resources - - def url_for(endpoint, **values): """Generates a URL to the given endpoint with the method provided. The endpoint is relative to the active module if modules are in use. @@ -365,7 +352,4 @@ class _PackageBoundObject(object): :param resource: the name of the resource. To access resources within subfolders use forward slashes as separator. """ - pkg_resources = get_pkg_resources() - if pkg_resources is None: - return open(os.path.join(self.root_path, resource), 'rb') - return pkg_resources.resource_stream(self.import_name, resource) + return open(os.path.join(self.root_path, resource), 'rb') From 15012af70017962c2a22bc3fe670b9cc50782366 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 12:48:37 +0200 Subject: [PATCH 040/207] Refactored static folder registration --- flask/app.py | 27 +++++++++------------------ flask/helpers.py | 24 ++++++++++++++++++++++++ flask/module.py | 28 +++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/flask/app.py b/flask/app.py index c902f276..a818ae62 100644 --- a/flask/app.py +++ b/flask/app.py @@ -10,7 +10,6 @@ """ import os -import posixpath from threading import Lock from datetime import timedelta, datetime from itertools import chain @@ -21,7 +20,7 @@ from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, NotFound from flask.helpers import _PackageBoundObject, url_for, get_flashed_messages, \ - _tojson_filter, send_file + _tojson_filter from flask.wrappers import Request, Response from flask.config import ConfigAttribute, Config from flask.ctx import _default_template_ctx_processor, _RequestContext @@ -101,6 +100,9 @@ class Flask(_PackageBoundObject): #: Path for the static files. If you don't want to use static files #: you can set this value to `None` in which case no URL rule is added #: and the development server will no longer serve any static files. + #: + #: This is the default used for application and modules unless a + #: different value is passed to the constructor. static_path = '/static' #: The debug flag. Set this to `True` to enable debugging of the @@ -190,8 +192,10 @@ class Flask(_PackageBoundObject): 'SERVER_NAME': None }) - def __init__(self, import_name): + def __init__(self, import_name, static_path=None): _PackageBoundObject.__init__(self, import_name) + if static_path is not None: + self.static_path = static_path #: The configuration dictionary as :class:`Config`. This behaves #: exactly like a regular dictionary but supports additional methods @@ -258,7 +262,8 @@ class Flask(_PackageBoundObject): #: app.url_map.converters['list'] = ListConverter self.url_map = Map() - if self.static_path is not None: + # if there is a static folder, register it for the application. + if self.has_static_folder: self.add_url_rule(self.static_path + '/', endpoint='static', view_func=self.send_static_file) @@ -377,20 +382,6 @@ class Flask(_PackageBoundObject): options.setdefault('use_debugger', self.debug) return run_simple(host, port, self, **options) - def send_static_file(self, filename): - """Function used internally to send static files from the static - folder to the browser. - - .. versionadded:: 0.5 - """ - filename = posixpath.normpath(filename) - if filename.startswith('../'): - raise NotFound() - filename = os.path.join(self.root_path, 'static', filename) - if not os.path.isfile(filename): - raise NotFound() - return send_file(filename, conditional=True) - def test_client(self): """Creates a test client for this application. For information about unit testing head over to :ref:`testing`. diff --git a/flask/helpers.py b/flask/helpers.py index 0afd4a85..518d953f 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -11,6 +11,7 @@ import os import sys +import posixpath import mimetypes from time import time from zlib import adler32 @@ -330,6 +331,29 @@ class _PackageBoundObject(object): #: Where is the app root located? self.root_path = _get_package_path(self.import_name) + @property + def has_static_folder(self): + """This is `True` if the package bound object's container has a + folder named ``'static'``. + + .. versionadded:: 0.5 + """ + return os.path.isdir(os.path.join(self.root_path, 'static')) + + def send_static_file(self, filename): + """Function used internally to send static files from the static + folder to the browser. + + .. versionadded:: 0.5 + """ + filename = posixpath.normpath(filename) + if filename.startswith('../'): + raise NotFound() + filename = os.path.join(self.root_path, 'static', filename) + if not os.path.isfile(filename): + raise NotFound() + return send_file(filename, conditional=True) + def open_resource(self, resource): """Opens a resource from the application's resource folder. To see how this works, consider the following folder structure:: diff --git a/flask/module.py b/flask/module.py index 85898511..cc904d20 100644 --- a/flask/module.py +++ b/flask/module.py @@ -12,6 +12,27 @@ from flask.helpers import _PackageBoundObject +def _register_module_static(module): + """Internal helper function that returns a function for recording + that registers the `send_static_file` function for the module on + the application of necessary. + """ + def _register_static(state): + # do not register the rule if the static folder of the + # module is the same as the one from the application. + if state.app.root_path == module.root_path: + return + path = static_path + if path is None: + path = state.app.static_path + if state.url_prefix: + path = state.url_prefix + path + state.app.add_url_rule(path + '/', + '%s.static' % module.name, + view_func=module.send_static_file) + return _register_static + + class _ModuleSetupState(object): def __init__(self, app, url_prefix=None): @@ -67,7 +88,8 @@ class Module(_PackageBoundObject): :ref:`working-with-modules` section. """ - def __init__(self, import_name, name=None, url_prefix=None): + def __init__(self, import_name, name=None, url_prefix=None, + static_path=None): if name is None: assert '.' in import_name, 'name required if package name ' \ 'does not point to a submodule' @@ -77,6 +99,10 @@ class Module(_PackageBoundObject): self.url_prefix = url_prefix self._register_events = [] + # if there is a static folder, register it for this module + if self.has_static_folder: + self._record(_register_module_static(self)) + def route(self, rule, **options): """Like :meth:`Flask.route` but for a module. The endpoint for the :func:`url_for` function is prefixed with the name of the module. From a38dcd5e2bed1bcf2c1fc319fa9c3bc35e360fe0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 13:42:00 +0200 Subject: [PATCH 041/207] Added support for loading templates from modules --- flask/app.py | 72 ++++++++++++++++++++++++++++++++++-------------- flask/helpers.py | 14 +++++++++- flask/module.py | 16 +++++------ 3 files changed, 72 insertions(+), 30 deletions(-) diff --git a/flask/app.py b/flask/app.py index a818ae62..fc46a4a3 100644 --- a/flask/app.py +++ b/flask/app.py @@ -14,7 +14,7 @@ from threading import Lock from datetime import timedelta, datetime from itertools import chain -from jinja2 import Environment, PackageLoader, FileSystemLoader +from jinja2 import Environment, BaseLoader, FileSystemLoader, TemplateNotFound from werkzeug import ImmutableDict, create_environ from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, NotFound @@ -32,13 +32,38 @@ from flask.module import _ModuleSetupState _logger_lock = Lock() -def _select_autoescape(filename): - """Returns `True` if autoescaping should be active for the given - template name. +class _DispatchingJinjaLoader(BaseLoader): + """A loader that looks for templates in the application and all + the module folders. """ - if filename is None: - return False - return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) + + def __init__(self, app): + self.app = app + + def get_source(self, environment, template): + name = template + loader = None + try: + module, name = template.split('/', 1) + loader = self.app.modules[module].jinja_loader + except (ValueError, KeyError): + pass + if loader is None: + loader = self.app.jinja_loader + try: + return loader.get_source(environment, name) + except TemplateNotFound: + # re-raise the exception with the correct fileame here. + # (the one that includes the prefix) + raise TemplateNotFound(template) + + def list_templates(self): + result = self.app.jinja_loader.list_templates() + for name, module in self.app.modules.iteritems(): + if module.jinja_loader is not None: + for template in module.jinja_loader.list_templates(): + result.append('%s/%s' % (name, template)) + return result class Flask(_PackageBoundObject): @@ -176,7 +201,6 @@ class Flask(_PackageBoundObject): #: Options that are passed directly to the Jinja2 environment. jinja_options = ImmutableDict( - autoescape=_select_autoescape, extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] ) @@ -245,6 +269,11 @@ class Flask(_PackageBoundObject): None: [_default_template_ctx_processor] } + #: all the loaded modules in a dictionary by name. + #: + #: .. versionadded:: 0.5 + self.modules = {} + #: The :class:`~werkzeug.routing.Map` for this instance. You can use #: this to change the routing converters after the class was created #: but before any routes are connected. Example:: @@ -269,8 +298,7 @@ class Flask(_PackageBoundObject): view_func=self.send_static_file) #: The Jinja2 environment. It is created from the - #: :attr:`jinja_options` and the loader that is returned - #: by the :meth:`create_jinja_loader` function. + #: :attr:`jinja_options`. self.jinja_env = self.create_jinja_environment() self.init_jinja_globals() @@ -315,16 +343,10 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.5 """ - return Environment(loader=self.create_jinja_loader(), - **self.jinja_options) - - def create_jinja_loader(self): - """Creates the Jinja loader. By default just a package loader for - the configured package is returned that looks up templates in the - `templates` folder. To add other loaders it's possible to - override this method. - """ - return FileSystemLoader(os.path.join(self.root_path, 'templates')) + options = dict(self.jinja_options) + if 'autoescape' not in options: + options['autoescape'] = self.select_jinja_autoescape + return Environment(loader=_DispatchingJinjaLoader(self), **options) def init_jinja_globals(self): """Called directly after the environment was created to inject @@ -339,6 +361,16 @@ class Flask(_PackageBoundObject): ) self.jinja_env.filters['tojson'] = _tojson_filter + def select_jinja_autoescape(self, filename): + """Returns `True` if autoescaping should be active for the given + template name. + + .. versionadded:: 0.5 + """ + if filename is None: + return False + return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) + def update_template_context(self, context): """Update the template context with some commonly used variables. This injects request, session and g into the template context. diff --git a/flask/helpers.py b/flask/helpers.py index 518d953f..d0b3ac23 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -27,7 +27,9 @@ except ImportError: except ImportError: json_available = False -from werkzeug import Headers, wrap_file, is_resource_modified +from werkzeug import Headers, wrap_file, is_resource_modified, cached_property + +from jinja2 import FileSystemLoader from flask.globals import session, _request_ctx_stack, current_app, request from flask.wrappers import Response @@ -340,6 +342,16 @@ class _PackageBoundObject(object): """ return os.path.isdir(os.path.join(self.root_path, 'static')) + @cached_property + def jinja_loader(self): + """The Jinja loader for this package bound object. + + .. versionadded:: 0.5 + """ + template_folder = os.path.join(self.root_path, 'templates') + if os.path.isdir(template_folder): + return FileSystemLoader(template_folder) + def send_static_file(self, filename): """Function used internally to send static files from the static folder to the browser. diff --git a/flask/module.py b/flask/module.py index cc904d20..ee1a342e 100644 --- a/flask/module.py +++ b/flask/module.py @@ -12,12 +12,14 @@ from flask.helpers import _PackageBoundObject -def _register_module_static(module): +def _register_module(module): """Internal helper function that returns a function for recording that registers the `send_static_file` function for the module on - the application of necessary. + the application of necessary. It also registers the module on + the application. """ - def _register_static(state): + def _register(state): + state.app.modules[module.name] = module # do not register the rule if the static folder of the # module is the same as the one from the application. if state.app.root_path == module.root_path: @@ -30,7 +32,7 @@ def _register_module_static(module): state.app.add_url_rule(path + '/', '%s.static' % module.name, view_func=module.send_static_file) - return _register_static + return _register class _ModuleSetupState(object): @@ -97,11 +99,7 @@ class Module(_PackageBoundObject): _PackageBoundObject.__init__(self, import_name) self.name = name self.url_prefix = url_prefix - self._register_events = [] - - # if there is a static folder, register it for this module - if self.has_static_folder: - self._record(_register_module_static(self)) + self._register_events = [_register_module(self)] def route(self, rule, **options): """Like :meth:`Flask.route` but for a module. The endpoint for the From a3c9494f67a0e89d44f459dac9eb9d0bc9b9025b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 13:49:58 +0200 Subject: [PATCH 042/207] Moved templating stuff into a separate module --- flask/__init__.py | 4 +-- flask/app.py | 38 ++--------------------- flask/helpers.py | 25 --------------- flask/templating.py | 74 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 62 deletions(-) create mode 100644 flask/templating.py diff --git a/flask/__init__.py b/flask/__init__.py index 85c8dd3f..f6015a64 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -18,7 +18,7 @@ from jinja2 import Markup, escape from flask.app import Flask, Request, Response from flask.config import Config from flask.helpers import url_for, jsonify, json_available, flash, send_file, \ - get_flashed_messages, render_template, render_template, render_template_string, \ - get_template_attribute, json + get_flashed_messages, get_template_attribute, json from flask.globals import current_app, g, request, session, _request_ctx_stack from flask.module import Module +from flask.templating import render_template, render_template_string diff --git a/flask/app.py b/flask/app.py index fc46a4a3..725e1f4c 100644 --- a/flask/app.py +++ b/flask/app.py @@ -14,7 +14,8 @@ from threading import Lock from datetime import timedelta, datetime from itertools import chain -from jinja2 import Environment, BaseLoader, FileSystemLoader, TemplateNotFound +from jinja2 import Environment + from werkzeug import ImmutableDict, create_environ from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, NotFound @@ -27,45 +28,12 @@ from flask.ctx import _default_template_ctx_processor, _RequestContext from flask.globals import _request_ctx_stack, request from flask.session import Session, _NullSession from flask.module import _ModuleSetupState +from flask.templating import _DispatchingJinjaLoader # a lock used for logger initialization _logger_lock = Lock() -class _DispatchingJinjaLoader(BaseLoader): - """A loader that looks for templates in the application and all - the module folders. - """ - - def __init__(self, app): - self.app = app - - def get_source(self, environment, template): - name = template - loader = None - try: - module, name = template.split('/', 1) - loader = self.app.modules[module].jinja_loader - except (ValueError, KeyError): - pass - if loader is None: - loader = self.app.jinja_loader - try: - return loader.get_source(environment, name) - except TemplateNotFound: - # re-raise the exception with the correct fileame here. - # (the one that includes the prefix) - raise TemplateNotFound(template) - - def list_templates(self): - result = self.app.jinja_loader.list_templates() - for name, module in self.app.modules.iteritems(): - if module.jinja_loader is not None: - for template in module.jinja_loader.list_templates(): - result.append('%s/%s' % (name, template)) - return result - - class Flask(_PackageBoundObject): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the diff --git a/flask/helpers.py b/flask/helpers.py index d0b3ac23..34c21cdd 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -290,31 +290,6 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, return rv -def render_template(template_name, **context): - """Renders a template from the template folder with the given - context. - - :param template_name: the name of the template to be rendered - :param context: the variables that should be available in the - context of the template. - """ - current_app.update_template_context(context) - return current_app.jinja_env.get_template(template_name).render(context) - - -def render_template_string(source, **context): - """Renders a template from the given template source string - with the given context. - - :param template_name: the sourcecode of the template to be - rendered - :param context: the variables that should be available in the - context of the template. - """ - current_app.update_template_context(context) - return current_app.jinja_env.from_string(source).render(context) - - def _get_package_path(name): """Returns the path to a package or cwd if that cannot be found.""" try: diff --git a/flask/templating.py b/flask/templating.py new file mode 100644 index 00000000..0bb704c1 --- /dev/null +++ b/flask/templating.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +""" + flask.templating + ~~~~~~~~~~~~~~~~ + + Implements the bridge to Jinja2. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound + +from flask.globals import _request_ctx_stack + + +class _DispatchingJinjaLoader(BaseLoader): + """A loader that looks for templates in the application and all + the module folders. + """ + + def __init__(self, app): + self.app = app + + def get_source(self, environment, template): + name = template + loader = None + try: + module, name = template.split('/', 1) + loader = self.app.modules[module].jinja_loader + except (ValueError, KeyError): + pass + if loader is None: + loader = self.app.jinja_loader + try: + return loader.get_source(environment, name) + except TemplateNotFound: + # re-raise the exception with the correct fileame here. + # (the one that includes the prefix) + raise TemplateNotFound(template) + + def list_templates(self): + result = self.app.jinja_loader.list_templates() + for name, module in self.app.modules.iteritems(): + if module.jinja_loader is not None: + for template in module.jinja_loader.list_templates(): + result.append('%s/%s' % (name, template)) + return result + + +def render_template(template_name, **context): + """Renders a template from the template folder with the given + context. + + :param template_name: the name of the template to be rendered + :param context: the variables that should be available in the + context of the template. + """ + ctx = _request_ctx_stack.top + ctx.app.update_template_context(context) + return ctx.app.jinja_env.get_template(template_name).render(context) + + +def render_template_string(source, **context): + """Renders a template from the given template source string + with the given context. + + :param template_name: the sourcecode of the template to be + rendered + :param context: the variables that should be available in the + context of the template. + """ + ctx = _request_ctx_stack.top + ctx.app.update_template_context(context) + return ctx.app.jinja_env.from_string(source).render(context) From 665fa2a32b5ff2b1c1887a48ed69329110a555f7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 14:01:56 +0200 Subject: [PATCH 043/207] More refactoring and moving stuff around --- flask/app.py | 6 +++--- flask/ctx.py | 11 ----------- flask/templating.py | 12 ++++++++++++ 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/flask/app.py b/flask/app.py index 725e1f4c..67b93cf7 100644 --- a/flask/app.py +++ b/flask/app.py @@ -24,11 +24,12 @@ from flask.helpers import _PackageBoundObject, url_for, get_flashed_messages, \ _tojson_filter from flask.wrappers import Request, Response from flask.config import ConfigAttribute, Config -from flask.ctx import _default_template_ctx_processor, _RequestContext +from flask.ctx import _RequestContext from flask.globals import _request_ctx_stack, request from flask.session import Session, _NullSession from flask.module import _ModuleSetupState -from flask.templating import _DispatchingJinjaLoader +from flask.templating import _DispatchingJinjaLoader, \ + _default_template_ctx_processor # a lock used for logger initialization _logger_lock = Lock() @@ -831,4 +832,3 @@ class Flask(_PackageBoundObject): def __call__(self, environ, start_response): """Shortcut for :attr:`wsgi_app`.""" return self.wsgi_app(environ, start_response) - diff --git a/flask/ctx.py b/flask/ctx.py index 1c538ecf..08eb1bf7 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -63,14 +63,3 @@ class _RequestContext(object): if not self.request.environ.get('flask._preserve_context') and \ (tb is None or not self.app.debug): self.pop() - -def _default_template_ctx_processor(): - """Default template context processor. Injects `request`, - `session` and `g`. - """ - reqctx = _request_ctx_stack.top - return dict( - request=reqctx.request, - session=reqctx.session, - g=reqctx.g - ) diff --git a/flask/templating.py b/flask/templating.py index 0bb704c1..3a4217dd 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -13,6 +13,18 @@ from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound from flask.globals import _request_ctx_stack +def _default_template_ctx_processor(): + """Default template context processor. Injects `request`, + `session` and `g`. + """ + reqctx = _request_ctx_stack.top + return dict( + request=reqctx.request, + session=reqctx.session, + g=reqctx.g + ) + + class _DispatchingJinjaLoader(BaseLoader): """A loader that looks for templates in the application and all the module folders. From d67a36cbdb20bfd9cd9931bceb8e6e3c27e9263f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 14:12:29 +0200 Subject: [PATCH 044/207] Added a testcase for the improved module support --- flask/module.py | 4 ++-- tests/flask_tests.py | 15 +++++++++++++++ tests/moduleapp/__init__.py | 7 +++++++ tests/moduleapp/apps/__init__.py | 0 tests/moduleapp/apps/admin/__init__.py | 9 +++++++++ tests/moduleapp/apps/admin/static/test.txt | 1 + tests/moduleapp/apps/admin/templates/index.html | 1 + tests/moduleapp/apps/frontend/__init__.py | 9 +++++++++ .../moduleapp/apps/frontend/templates/index.html | 1 + 9 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 tests/moduleapp/__init__.py create mode 100644 tests/moduleapp/apps/__init__.py create mode 100644 tests/moduleapp/apps/admin/__init__.py create mode 100644 tests/moduleapp/apps/admin/static/test.txt create mode 100644 tests/moduleapp/apps/admin/templates/index.html create mode 100644 tests/moduleapp/apps/frontend/__init__.py create mode 100644 tests/moduleapp/apps/frontend/templates/index.html diff --git a/flask/module.py b/flask/module.py index ee1a342e..d106e69b 100644 --- a/flask/module.py +++ b/flask/module.py @@ -12,7 +12,7 @@ from flask.helpers import _PackageBoundObject -def _register_module(module): +def _register_module(module, static_path): """Internal helper function that returns a function for recording that registers the `send_static_file` function for the module on the application of necessary. It also registers the module on @@ -99,7 +99,7 @@ class Module(_PackageBoundObject): _PackageBoundObject.__init__(self, import_name) self.name = name self.url_prefix = url_prefix - self._register_events = [_register_module(self)] + self._register_events = [_register_module(self, static_path)] def route(self, rule, **options): """Like :meth:`Flask.route` but for a module. The endpoint for the diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 7e2c9246..384e2e91 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -630,6 +630,21 @@ class ModuleTestCase(unittest.TestCase): assert rv.status_code == 500 assert 'internal server error' == rv.data + def test_templates_and_static(self): + from moduleapp import app + c = app.test_client() + + rv = c.get('/') + assert rv.data == 'Hello from the Frontend' + rv = c.get('/admin/') + assert rv.data == 'Hello from the Admin' + rv = c.get('/admin/static/test.txt') + assert rv.data.strip() == 'Admin File' + + with app.test_request_context(): + assert flask.url_for('admin.static', filename='test.txt') \ + == '/admin/static/test.txt' + class SendfileTestCase(unittest.TestCase): diff --git a/tests/moduleapp/__init__.py b/tests/moduleapp/__init__.py new file mode 100644 index 00000000..35e82d4e --- /dev/null +++ b/tests/moduleapp/__init__.py @@ -0,0 +1,7 @@ +from flask import Flask + +app = Flask(__name__) +from moduleapp.apps.admin import admin +from moduleapp.apps.frontend import frontend +app.register_module(admin) +app.register_module(frontend) diff --git a/tests/moduleapp/apps/__init__.py b/tests/moduleapp/apps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/moduleapp/apps/admin/__init__.py b/tests/moduleapp/apps/admin/__init__.py new file mode 100644 index 00000000..98af2b26 --- /dev/null +++ b/tests/moduleapp/apps/admin/__init__.py @@ -0,0 +1,9 @@ +from flask import Module, render_template + + +admin = Module(__name__, url_prefix='/admin') + + +@admin.route('/') +def index(): + return render_template('admin/index.html') diff --git a/tests/moduleapp/apps/admin/static/test.txt b/tests/moduleapp/apps/admin/static/test.txt new file mode 100644 index 00000000..f220d22f --- /dev/null +++ b/tests/moduleapp/apps/admin/static/test.txt @@ -0,0 +1 @@ +Admin File diff --git a/tests/moduleapp/apps/admin/templates/index.html b/tests/moduleapp/apps/admin/templates/index.html new file mode 100644 index 00000000..eeec199a --- /dev/null +++ b/tests/moduleapp/apps/admin/templates/index.html @@ -0,0 +1 @@ +Hello from the Admin diff --git a/tests/moduleapp/apps/frontend/__init__.py b/tests/moduleapp/apps/frontend/__init__.py new file mode 100644 index 00000000..f83581e7 --- /dev/null +++ b/tests/moduleapp/apps/frontend/__init__.py @@ -0,0 +1,9 @@ +from flask import Module, render_template + + +frontend = Module(__name__) + + +@frontend.route('/') +def index(): + return render_template('frontend/index.html') diff --git a/tests/moduleapp/apps/frontend/templates/index.html b/tests/moduleapp/apps/frontend/templates/index.html new file mode 100644 index 00000000..a062d713 --- /dev/null +++ b/tests/moduleapp/apps/frontend/templates/index.html @@ -0,0 +1 @@ +Hello from the Frontend From f694456ff623941020da16839b7edd95248ed26e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 14:13:30 +0200 Subject: [PATCH 045/207] Documented changes --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 34cd23d8..64ec43ba 100644 --- a/CHANGES +++ b/CHANGES @@ -21,6 +21,7 @@ Codename to be decided, release date to be announced. do conditional responses builtin. - (temporarily) dropped support for zipped applications. This was a rarely used feature and led to some confusing behaviour. +- added support for per-package template and static-file directories. Version 0.4 ----------- From 31d359694aeb4ef8770e7d2ba14ae479c23006a2 Mon Sep 17 00:00:00 2001 From: florentx Date: Sun, 4 Jul 2010 19:02:27 +0800 Subject: [PATCH 046/207] Fix typos --- docs/_themes | 1 - docs/unicode.rst | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) delete mode 160000 docs/_themes diff --git a/docs/_themes b/docs/_themes deleted file mode 160000 index 21cf0743..00000000 --- a/docs/_themes +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 21cf07433147212ee6c8ab203dfa648a9239c66f diff --git a/docs/unicode.rst b/docs/unicode.rst index 7db462b8..5b4202ad 100644 --- a/docs/unicode.rst +++ b/docs/unicode.rst @@ -8,7 +8,7 @@ should probably read `The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets `_. This part of the documentation just tries to cover the very basics so that you have a -pleasent experience with unicode related things. +pleasant experience with unicode related things. Automatic Conversion -------------------- @@ -54,7 +54,7 @@ unicode. What does working with unicode in Python 2.x mean? UTF-8 for this purpose. To tell the interpreter your encoding you can put the ``# -*- coding: utf-8 -*-`` into the first or second line of your Python source file. -- Jinja is configured to decode the template files from UTF08. So make +- Jinja is configured to decode the template files from UTF-8. So make sure to tell your editor to save the file as UTF-8 there as well. Encoding and Decoding Yourself @@ -63,12 +63,12 @@ Encoding and Decoding Yourself If you are talking with a filesystem or something that is not really based on unicode you will have to ensure that you decode properly when working with unicode interface. So for example if you want to load a file on the -filesystem and embedd it into a Jinja2 template you will have to decode it -form the encoding of that file. Here the old problem that textfiles do +filesystem and embed it into a Jinja2 template you will have to decode it +from the encoding of that file. Here the old problem that text files do not specify their encoding comes into play. So do yourself a favour and -limit yourself to UTF-8 for textfiles as well. +limit yourself to UTF-8 for text files as well. -Anyways. To load such a file with unicode you can use the builtin +Anyways. To load such a file with unicode you can use the built-in :meth:`str.decode` method:: def read_file(filename, charset='utf-8'): @@ -104,4 +104,4 @@ set your editor to store as UTF-8: 3. Select "UTF-8 without BOM" as encoding It is also recommended to use the Unix newline format, you can select - it in the same panel but this not a requirement. + it in the same panel but this is not a requirement. From b551f15b22c1d7d7749901e8d186246fb12038e8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 14:17:29 +0200 Subject: [PATCH 047/207] Restored 2.5 compatibility{ --- flask/app.py | 2 ++ flask/config.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/flask/app.py b/flask/app.py index 67b93cf7..983dde8f 100644 --- a/flask/app.py +++ b/flask/app.py @@ -9,6 +9,8 @@ :license: BSD, see LICENSE for more details. """ +from __future__ import with_statement + import os from threading import Lock from datetime import timedelta, datetime diff --git a/flask/config.py b/flask/config.py index 7918de01..cf9ad541 100644 --- a/flask/config.py +++ b/flask/config.py @@ -9,6 +9,8 @@ :license: BSD, see LICENSE for more details. """ +from __future__ import with_statement + import os import sys From 2b00ec4017ee6b930bb805c294a459f112e4bc80 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 14:19:17 +0200 Subject: [PATCH 048/207] Small cleanups --- flask/app.py | 4 ++-- flask/helpers.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flask/app.py b/flask/app.py index 983dde8f..c20fade9 100644 --- a/flask/app.py +++ b/flask/app.py @@ -363,9 +363,9 @@ class Flask(_PackageBoundObject): .. admonition:: Keep in Mind - Flask will supress any server error with a generic error page + Flask will suppress any server error with a generic error page unless it is in debug mode. As such to enable just the - interactive debugger without the code reloading, you ahve to + interactive debugger without the code reloading, you have to invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. Setting ``use_debugger`` to `True` without being in debug mode won't catch any exceptions because there won't be any to diff --git a/flask/helpers.py b/flask/helpers.py index 34c21cdd..3d23842c 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -214,8 +214,8 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, .. versionadded:: 0.2 .. versionadded:: 0.5 - The `add_etags`, `cache_timeout` and `conditional` parameters were added. - The default behaviour is now to attach etags. + The `add_etags`, `cache_timeout` and `conditional` parameters were + added. The default behaviour is now to attach etags. :param filename_or_fp: the filename of the file to send. This is relative to the :attr:`~Flask.root_path` if a From 362cf493c42deaa417aa42a402852679ac349a4d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 14:20:20 +0200 Subject: [PATCH 049/207] Documented the create_jinja_loader removal --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 64ec43ba..32f889a3 100644 --- a/CHANGES +++ b/CHANGES @@ -22,6 +22,8 @@ Codename to be decided, release date to be announced. - (temporarily) dropped support for zipped applications. This was a rarely used feature and led to some confusing behaviour. - added support for per-package template and static-file directories. +- removed support for `create_jinja_loader` which is no longer used + in 0.5 due to the improved module support. Version 0.4 ----------- From 9e1111c2fbc2c7670eb489df0262a90417b96019 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 17:21:13 +0200 Subject: [PATCH 050/207] Fixed JSON availability test. This fixes #77 --- flask/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index f6015a64..02141f56 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -17,8 +17,12 @@ from jinja2 import Markup, escape from flask.app import Flask, Request, Response from flask.config import Config -from flask.helpers import url_for, jsonify, json_available, flash, send_file, \ - get_flashed_messages, get_template_attribute, json +from flask.helpers import url_for, jsonify, json_available, flash, \ + send_file, get_flashed_messages, get_template_attribute from flask.globals import current_app, g, request, session, _request_ctx_stack from flask.module import Module from flask.templating import render_template, render_template_string + +# only import json if it's available +if json_available: + from flask.helpers import json From bca1acf1f38b0bb6d461fbbc486d6c95eafe0b30 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 19:57:53 +0200 Subject: [PATCH 051/207] Rewrote becoming big and foreword --- docs/becomingbig.rst | 100 ++++++++++++++++++++++++++----------------- docs/foreword.rst | 28 ++++++------ 2 files changed, 72 insertions(+), 56 deletions(-) diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst index 8ad125de..916aa324 100644 --- a/docs/becomingbig.rst +++ b/docs/becomingbig.rst @@ -3,54 +3,74 @@ Becoming Big ============ -Your application is becoming more and more complex? Flask is really not -designed for large scale applications and does not attempt to do so, but -that does not mean you picked the wrong tool in the first place. +Your application is becoming more and more complex? If you suddenly +realize that Flask does things in a way that does not work out for your +application there are ways to deal with that. Flask is powered by Werkzeug and Jinja2, two libraries that are in use at a number of large websites out there and all Flask does is bring those -two together. Being a microframework, Flask is literally a single file. -What that means for large applications is that it's probably a good idea -to take the code from Flask and put it into a new module within the -applications and expand on that. +two together. Being a microframework Flask does not do much more than +combinding existing libraries - there is not a lot of code involved. +What that means for large applications is that it's very easy to take the +code from Flask and put it into a new module within the applications and +expand on that. -What Could Be Improved? ------------------------ +Flask is designed to be extended and modified in a couple of different +ways: -For instance it makes a lot of sense to change the way endpoints (the -names of the functions / URL rules) are handled to also take the module -name into account. Right now the function name is the URL name, but -imagine you have a large application consisting of multiple components. -In that case, it makes a lot of sense to use dotted names for the URL -endpoints. +- Subclassing. The majority of functionality can be changed by creating + a new subclass of the :class:`~flask.Flask` class and overriding + some methods. -Here are some suggestions for how Flask can be modified to better -accommodate large-scale applications: +- Flask extensions. For a lot of reusable functionality you can create + extensions. -- get rid of the decorator function registering which causes a lot - of troubles for applications that have circular dependencies. It - also requires that the whole application is imported when the system - initializes or certain URLs will not be available right away. A - better solution would be to have one module with all URLs in there and - specifying the target functions explicitly or by name and importing - them when needed. -- switch to explicit request object passing. This requires more typing - (because you now have something to pass around) but it makes it a - whole lot easier to debug hairy situations and to test the code. -- integrate the `Babel`_ i18n package or `SQLAlchemy`_ directly into the - core framework. +- Forking. If nothing else works out you can just take the Flask + codebase at a given point and copy/paste it into your application + and change it. Flask is designed with that in mind and makes this + incredible easy. You just have to take the package and copy it + into your application's code and rename it (for example to + `framework`). Then you can start modifying the code in there. -.. _Babel: http://babel.edgewall.org/ -.. _SQLAlchemy: http://www.sqlalchemy.org/ +Why consider Forking? +--------------------- -Why does Flask not do all that by Default? ------------------------------------------- +The majority of code of Flask is within Werkzeug and Jinja2. These +libraries do the majority of the work. Flask is just the paste that glues +those together. For every project there is the point where the underlying +framework gets in the way (due to assumptions the original developers +had). This is natural because if this would not be the case, the +framework would be a very complex system to begin with which causes a +steep learning curve and a lot of user frustration. -There is a huge difference between a small application that only has to -handle a couple of requests per second and with an overall code complexity -of less than 4000 lines of code and something of larger scale. At some -point it becomes important to integrate external systems, different -storage backends and more. +This is not unique to Flask. Many people use patched and modified +versions of their framework to counter shortcomings. This idea is also +reflected in the license of Flask. You don't have to contribute any +changes back if you decide to modify the framework. -If Flask was designed with all these contingencies in mind, it would be a -much more complex framework and harder to get started with. +The downside of forking is of course that Flask extensions will most +likely break because the new framework has a different import name and +because of that forking should be the last resort. + +Scaling like a Pro +------------------ + +For many web applications the complexity of the code is less an issue than +the scaling for the number of users or data entries expected. Flask by +itself is only limited in terms of scaling by your application code, the +data store you want to use and the Python implementation and webserver you +are running on. + +Scaling well means for example that if you double the amount of servers +you get about twice the performance. Scaling bad means that if you add a +new server the application won't perform any better or would not even +support a second server. + +There is only one limiting factor regarding scaling in Flask which are +the context local proxies. They depend on context which in Flask is +defined as being either a thread or a greenlet. Separate processes are +fine as well. If your server uses some kind of concurrency that is not +based on threads or greenlets, Flask will no longer be able to support +these global proxies. However the majority of servers are using either +threads, greenlets or separate processes to achieve concurrency which are +all methods well supported by the underlying Werkzeug library. diff --git a/docs/foreword.rst b/docs/foreword.rst index de6b4980..6b9d99b2 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -17,24 +17,20 @@ may be necessary in larger or more complex applications. For example, Flask uses thread-local objects internally so that you don't have to pass objects around from function to function within a request in order to stay threadsafe. While this is a really easy approach and saves -you a lot of time, it also does not scale well to large applications. -It's especially painful for more complex unittests, and when you suddenly -have to deal with code being executed outside of the context of a request, -such as in cron jobs. +you a lot of time, it might also cause some troubles for very large +applications because changes on these thread-local objects can happen +anywhere in the same thread. -Flask provides some tools to deal with the downsides of this approach, but -the core problem remains. Flask is also based on convention over -configuration, which means that many things are preconfigured and will -work well for smaller applications but not so well for larger ones. For -example, by convention, templates and static files are in subdirectories -within the Python source tree of the application. +Flask provides some tools to deal with the downsides of this approach but +it might be an issue for larger applications. Flask is also based on +convention over configuration, which means that many things are +preconfigured and will work well for smaller applications but not so well +for larger ones. For example, by convention, templates and static files +are in subdirectories within the Python source tree of the application. -But don't worry if your application suddenly grows larger -and you're afraid Flask might not grow with it. Even with -larger frameworks, you'll eventually discover that you need -something the framework just cannot do for you without modification. -If you are ever in that situation, check out the :ref:`becomingbig` -chapter. +However Flask is not much code and built in a very solid foundation and +with that very easy to adapt for large applications. If you are +interested in that, check out the :ref:`becomingbig` chapter. A Framework and an Example -------------------------- From 80eb6cfffc3f3fe6e448aa0e26ba1566d4c42917 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 20:00:23 +0200 Subject: [PATCH 052/207] Switched to relative imports in the package --- flask/__init__.py | 14 +++++++------- flask/app.py | 16 ++++++++-------- flask/ctx.py | 4 ++-- flask/helpers.py | 4 ++-- flask/templating.py | 2 +- flask/wrappers.py | 2 +- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index 02141f56..76eed660 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -15,14 +15,14 @@ from werkzeug import abort, redirect from jinja2 import Markup, escape -from flask.app import Flask, Request, Response -from flask.config import Config -from flask.helpers import url_for, jsonify, json_available, flash, \ +from .app import Flask, Request, Response +from .config import Config +from .helpers import url_for, jsonify, json_available, flash, \ send_file, get_flashed_messages, get_template_attribute -from flask.globals import current_app, g, request, session, _request_ctx_stack -from flask.module import Module -from flask.templating import render_template, render_template_string +from .globals import current_app, g, request, session, _request_ctx_stack +from .module import Module +from .templating import render_template, render_template_string # only import json if it's available if json_available: - from flask.helpers import json + from .helpers import json diff --git a/flask/app.py b/flask/app.py index c20fade9..87f3e9d3 100644 --- a/flask/app.py +++ b/flask/app.py @@ -22,15 +22,15 @@ from werkzeug import ImmutableDict, create_environ from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, NotFound -from flask.helpers import _PackageBoundObject, url_for, get_flashed_messages, \ +from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ _tojson_filter -from flask.wrappers import Request, Response -from flask.config import ConfigAttribute, Config -from flask.ctx import _RequestContext -from flask.globals import _request_ctx_stack, request -from flask.session import Session, _NullSession -from flask.module import _ModuleSetupState -from flask.templating import _DispatchingJinjaLoader, \ +from .wrappers import Request, Response +from .config import ConfigAttribute, Config +from .ctx import _RequestContext +from .globals import _request_ctx_stack, request +from .session import Session, _NullSession +from .module import _ModuleSetupState +from .templating import _DispatchingJinjaLoader, \ _default_template_ctx_processor # a lock used for logger initialization diff --git a/flask/ctx.py b/flask/ctx.py index 08eb1bf7..90ea50db 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -11,8 +11,8 @@ from werkzeug.exceptions import HTTPException -from flask.globals import _request_ctx_stack -from flask.session import _NullSession +from .globals import _request_ctx_stack +from .session import _NullSession class _RequestGlobals(object): diff --git a/flask/helpers.py b/flask/helpers.py index 3d23842c..2303b58d 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -31,8 +31,8 @@ from werkzeug import Headers, wrap_file, is_resource_modified, cached_property from jinja2 import FileSystemLoader -from flask.globals import session, _request_ctx_stack, current_app, request -from flask.wrappers import Response +from .globals import session, _request_ctx_stack, current_app, request +from .wrappers import Response def _assert_have_json(): diff --git a/flask/templating.py b/flask/templating.py index 3a4217dd..d1e75959 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -10,7 +10,7 @@ """ from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound -from flask.globals import _request_ctx_stack +from .globals import _request_ctx_stack def _default_template_ctx_processor(): diff --git a/flask/wrappers.py b/flask/wrappers.py index d962a563..7605ea13 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -12,7 +12,7 @@ from werkzeug import Request as RequestBase, Response as ResponseBase, \ cached_property -from helpers import json +from .helpers import json class Request(RequestBase): From 51a89bf35ec6231d55f5caa8e12484e801f593fe Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 20:07:03 +0200 Subject: [PATCH 053/207] Restored 2.5 compatibility and actual fix for the json problem --- .gitignore | 2 ++ flask/helpers.py | 6 +++++- flask/module.py | 2 +- flask/wrappers.py | 3 +-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 148e5029..4844be8e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ *.pyc *.pyo env +env* dist +*.egg *.egg-info _mailinglist diff --git a/flask/helpers.py b/flask/helpers.py index 2303b58d..dcf25baf 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -19,6 +19,7 @@ from zlib import adler32 # try to load the best simplejson implementation available. If JSON # is not installed, we add a failing class. json_available = True +json = None try: import simplejson as json except ImportError: @@ -32,7 +33,6 @@ from werkzeug import Headers, wrap_file, is_resource_modified, cached_property from jinja2 import FileSystemLoader from .globals import session, _request_ctx_stack, current_app, request -from .wrappers import Response def _assert_have_json(): @@ -364,3 +364,7 @@ class _PackageBoundObject(object): subfolders use forward slashes as separator. """ return open(os.path.join(self.root_path, resource), 'rb') + + +# circular dependencies between wrappers and helpers +from .wrappers import Response diff --git a/flask/module.py b/flask/module.py index d106e69b..dfd30675 100644 --- a/flask/module.py +++ b/flask/module.py @@ -9,7 +9,7 @@ :license: BSD, see LICENSE for more details. """ -from flask.helpers import _PackageBoundObject +from .helpers import _PackageBoundObject def _register_module(module, static_path): diff --git a/flask/wrappers.py b/flask/wrappers.py index 7605ea13..1dcf23d1 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -12,7 +12,7 @@ from werkzeug import Request as RequestBase, Response as ResponseBase, \ cached_property -from .helpers import json +from .helpers import json, _assert_have_json class Request(RequestBase): @@ -52,7 +52,6 @@ class Request(RequestBase): parsed JSON data. """ if __debug__: - from flask.helpers import _assert_have_json _assert_have_json() if self.mimetype == 'application/json': return json.loads(self.data) From f1cde5bbfcb7f0466ad35511ffe2e31ea57756d6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 20:11:54 +0200 Subject: [PATCH 054/207] Added more versionadded directives --- flask/app.py | 9 +++++++++ flask/module.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/flask/app.py b/flask/app.py index 87f3e9d3..9cdf9274 100644 --- a/flask/app.py +++ b/flask/app.py @@ -83,6 +83,15 @@ class Flask(_PackageBoundObject): up, that debugging information is lost. (For example it would only pick up SQL queries in `yourapplicaiton.app` and not `yourapplication.views.frontend`) + + .. versionadded:: 0.5 + The `static_path` parameter was added. + + :param import_name: the name of the application package + :param static_path: can be used to specify a different path for the + static files on the web. Defaults to ``/static``. + This does not affect the folder the files are served + *from*. """ #: The class that is used for request objects. See :class:`~flask.Request` diff --git a/flask/module.py b/flask/module.py index dfd30675..6a4f0fb3 100644 --- a/flask/module.py +++ b/flask/module.py @@ -88,6 +88,21 @@ class Module(_PackageBoundObject): For a gentle introduction into modules, checkout the :ref:`working-with-modules` section. + + .. versionadded:: 0.5 + The `static_path` parameter was added. + + :param import_name: the name of the Python package or module + implementing this :class:`Module`. + :param name: the internal short name for the module. Unless specified + the rightmost part of the import name + :param url_prefix: an optional string that is used to prefix all the + URL rules of this module. This can also be specified + when registering the module with the application. + :param static_path: can be used to specify a different path for the + static files on the web. Defaults to ``/static``. + This does not affect the folder the files are served + *from*. """ def __init__(self, import_name, name=None, url_prefix=None, From 8945a97a42d6475b1ee9fcb2689dc65dfce9828f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 20:36:34 +0200 Subject: [PATCH 055/207] fixed possible security problem in module branch --- flask/helpers.py | 3 ++- tests/flask_tests.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index dcf25baf..3387f623 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -29,6 +29,7 @@ except ImportError: json_available = False from werkzeug import Headers, wrap_file, is_resource_modified, cached_property +from werkzeug.exceptions import NotFound from jinja2 import FileSystemLoader @@ -334,7 +335,7 @@ class _PackageBoundObject(object): .. versionadded:: 0.5 """ filename = posixpath.normpath(filename) - if filename.startswith('../'): + if filename.startswith(('/', '../')): raise NotFound() filename = os.path.join(self.root_path, 'static', filename) if not os.path.isfile(filename): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 384e2e91..b3835302 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -20,6 +20,7 @@ from logging import StreamHandler from contextlib import contextmanager from datetime import datetime from werkzeug import parse_date, parse_options_header +from werkzeug.exceptions import NotFound from cStringIO import StringIO example_path = os.path.join(os.path.dirname(__file__), '..', 'examples') @@ -645,6 +646,25 @@ class ModuleTestCase(unittest.TestCase): assert flask.url_for('admin.static', filename='test.txt') \ == '/admin/static/test.txt' + def test_safe_access(self): + from moduleapp import app + + with app.test_request_context(): + f = app.view_functions['admin.static'] + + try: + rv = f('/etc/passwd') + except NotFound: + pass + else: + assert 0, 'expected exception' + try: + rv = f('../__init__.py') + except NotFound: + pass + else: + assert 0, 'expected exception' + class SendfileTestCase(unittest.TestCase): From 09e7b5eb2f3a3da0b09ffe4cd3cad8b028530874 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 20:40:58 +0200 Subject: [PATCH 056/207] Added a chapter for 0.5 in upgrading.rst --- docs/upgrading.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 20604c8c..7ee35176 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -19,6 +19,30 @@ installation, make sure to pass it the ``-U`` parameter:: $ easy_install -U Flask +Version 0.5 +----------- + +Flask 0.5 is the first release that comes as a Python package instead of a +single module. There were a couple of internal refactoring so if you +depend on undocumented internal details you probably have to adapt the +imports. + +The following changes may be relevant to your application: + +- autoescaping no longer happens for all templates. Instead it is + configured to only happen on files ending with ``.html``, ``.htm``, + ``.xml`` and ``.xhtml``. If you have templates with different + extensions you should override the + :meth:`~flask.Flask.select_jinja_autoescape` method. +- Flask no longer supports zipped applications in this release. This + functionality might come back in future releases if there is demand + for this feature. Removing support for this makes the Flask internal + code easier to understand and fixes a couple of small issues that make + debugging harder than necessary. +- The `create_jinja_loader` function is gone. If you want to customize + the Jinja loader now, use the + :meth:`~flask.Flask.create_jinja_environment` method instead. + Version 0.4 ----------- From b7109865a352e6bbee55247731136a46e1bab04d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 22:57:49 +0200 Subject: [PATCH 057/207] Removed circular dependency by going over a proxy. This is the better solution anyway --- flask/helpers.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index 3387f623..8f2a2083 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -268,8 +268,8 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, file = open(filename, 'rb') data = wrap_file(request.environ, file) - rv = Response(data, mimetype=mimetype, headers=headers, - direct_passthrough=True) + rv = current_app.response_class(data, mimetype=mimetype, headers=headers, + direct_passthrough=True) rv.cache_control.public = True if cache_timeout: @@ -365,7 +365,3 @@ class _PackageBoundObject(object): subfolders use forward slashes as separator. """ return open(os.path.join(self.root_path, resource), 'rb') - - -# circular dependencies between wrappers and helpers -from .wrappers import Response From ac13deff401069c3854acca10c926119c6e1cbe0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 5 Jul 2010 10:23:35 +0200 Subject: [PATCH 058/207] Re-added support for folder with static files, refactored static file sending --- flask/__init__.py | 3 +- flask/app.py | 2 +- flask/helpers.py | 36 +++++++++++++++---- flask/module.py | 2 +- tests/flask_tests.py | 2 ++ .../moduleapp/apps/admin/static/css/test.css | 1 + 6 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 tests/moduleapp/apps/admin/static/css/test.css diff --git a/flask/__init__.py b/flask/__init__.py index 76eed660..f4617271 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -18,7 +18,8 @@ from jinja2 import Markup, escape from .app import Flask, Request, Response from .config import Config from .helpers import url_for, jsonify, json_available, flash, \ - send_file, get_flashed_messages, get_template_attribute + send_file, send_from_directory, get_flashed_messages, \ + get_template_attribute from .globals import current_app, g, request, session, _request_ctx_stack from .module import Module from .templating import render_template, render_template_string diff --git a/flask/app.py b/flask/app.py index 9cdf9274..8e559d4e 100644 --- a/flask/app.py +++ b/flask/app.py @@ -273,7 +273,7 @@ class Flask(_PackageBoundObject): # if there is a static folder, register it for the application. if self.has_static_folder: - self.add_url_rule(self.static_path + '/', + self.add_url_rule(self.static_path + '/', endpoint='static', view_func=self.send_static_file) diff --git a/flask/helpers.py b/flask/helpers.py index 8f2a2083..65506698 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -291,6 +291,33 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, return rv +def send_from_directory(directory, filename, **options): + """Send a file from a given directory with :func:`send_file`. This + is a secure way to quickly expose static files from an upload folder + or something similar. + + Example usage:: + + @app.route('/uploads/') + def download_file(filename): + return send_from_directory(app.config['UPLOAD_FOLDER'], + filename, as_attachment=True) + + :param directory: the directory where all the files are stored. + :param filename: the filename relative to that directory to + download. + :param options: optional keyword arguments that are directly + forwarded to :func:`send_file`. + """ + filename = posixpath.normpath(filename) + if filename.startswith(('/', '../')): + raise NotFound() + filename = os.path.join(directory, filename) + if not os.path.isfile(filename): + raise NotFound() + return send_file(filename, conditional=True, **options) + + def _get_package_path(name): """Returns the path to a package or cwd if that cannot be found.""" try: @@ -334,13 +361,8 @@ class _PackageBoundObject(object): .. versionadded:: 0.5 """ - filename = posixpath.normpath(filename) - if filename.startswith(('/', '../')): - raise NotFound() - filename = os.path.join(self.root_path, 'static', filename) - if not os.path.isfile(filename): - raise NotFound() - return send_file(filename, conditional=True) + return send_from_directory(os.path.join(self.root_path, 'static'), + filename) def open_resource(self, resource): """Opens a resource from the application's resource folder. To see diff --git a/flask/module.py b/flask/module.py index 6a4f0fb3..7bebac4b 100644 --- a/flask/module.py +++ b/flask/module.py @@ -29,7 +29,7 @@ def _register_module(module, static_path): path = state.app.static_path if state.url_prefix: path = state.url_prefix + path - state.app.add_url_rule(path + '/', + state.app.add_url_rule(path + '/', '%s.static' % module.name, view_func=module.send_static_file) return _register diff --git a/tests/flask_tests.py b/tests/flask_tests.py index b3835302..129ec3b2 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -641,6 +641,8 @@ class ModuleTestCase(unittest.TestCase): assert rv.data == 'Hello from the Admin' rv = c.get('/admin/static/test.txt') assert rv.data.strip() == 'Admin File' + rv = c.get('/admin/static/css/test.css') + assert rv.data.strip() == '/* nested file */' with app.test_request_context(): assert flask.url_for('admin.static', filename='test.txt') \ diff --git a/tests/moduleapp/apps/admin/static/css/test.css b/tests/moduleapp/apps/admin/static/css/test.css new file mode 100644 index 00000000..b9f564de --- /dev/null +++ b/tests/moduleapp/apps/admin/static/css/test.css @@ -0,0 +1 @@ +/* nested file */ From c34b03e9a677e467ea420d4286d4350ed429a4b8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 5 Jul 2010 10:25:51 +0200 Subject: [PATCH 059/207] Documented send_from_directory --- CHANGES | 1 + docs/api.rst | 2 ++ flask/helpers.py | 9 +++++++++ 3 files changed, 12 insertions(+) diff --git a/CHANGES b/CHANGES index 32f889a3..10ed8e9c 100644 --- a/CHANGES +++ b/CHANGES @@ -24,6 +24,7 @@ Codename to be decided, release date to be announced. - added support for per-package template and static-file directories. - removed support for `create_jinja_loader` which is no longer used in 0.5 due to the improved module support. +- added a helper function to expose files from any directory. Version 0.4 ----------- diff --git a/docs/api.rst b/docs/api.rst index b6c81d10..d7f887e4 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -230,6 +230,8 @@ Useful Functions and Classes .. autofunction:: send_file +.. autofunction:: send_from_directory + .. autofunction:: escape .. autoclass:: Markup diff --git a/flask/helpers.py b/flask/helpers.py index 65506698..560492c6 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -303,6 +303,15 @@ def send_from_directory(directory, filename, **options): return send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=True) + .. admonition:: Sending files and Performance + + It is strongly recommended to activate either `X-Sendfile` support in + your webserver or (if no authentication happens) to tell the webserver + to serve files for the given path on its own without calling into the + web application for improved performance. + + .. versionadded:: 0.5 + :param directory: the directory where all the files are stored. :param filename: the filename relative to that directory to download. From df3f8940c30447f28f2ddf2a5f764cf543755f0d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 5 Jul 2010 10:31:18 +0200 Subject: [PATCH 060/207] Added separate module for testing --- flask/app.py | 25 +++---------------------- flask/ctx.py | 1 + flask/module.py | 2 +- flask/testing.py | 39 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 23 deletions(-) create mode 100644 flask/testing.py diff --git a/flask/app.py b/flask/app.py index 8e559d4e..dbc102f7 100644 --- a/flask/app.py +++ b/flask/app.py @@ -18,7 +18,7 @@ from itertools import chain from jinja2 import Environment -from werkzeug import ImmutableDict, create_environ +from werkzeug import ImmutableDict from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, NotFound @@ -409,27 +409,7 @@ class Flask(_PackageBoundObject): .. versionchanged:: 0.4 added support for `with` block usage for the client. """ - from werkzeug import Client - class FlaskClient(Client): - preserve_context = context_preserved = False - def open(self, *args, **kwargs): - if self.context_preserved: - _request_ctx_stack.pop() - self.context_preserved = False - kwargs.setdefault('environ_overrides', {}) \ - ['flask._preserve_context'] = self.preserve_context - old = _request_ctx_stack.top - try: - return Client.open(self, *args, **kwargs) - finally: - self.context_preserved = _request_ctx_stack.top is not old - def __enter__(self): - self.preserve_context = True - return self - def __exit__(self, exc_type, exc_value, tb): - self.preserve_context = False - if self.context_preserved: - _request_ctx_stack.pop() + from flask.testing import FlaskClient return FlaskClient(self, self.response_class, use_cookies=True) def open_session(self, request): @@ -838,6 +818,7 @@ class Flask(_PackageBoundObject): :func:`werkzeug.create_environ` for more information, this function accepts the same arguments). """ + from werkzeug import create_environ return self.request_context(create_environ(*args, **kwargs)) def __call__(self, environ, start_response): diff --git a/flask/ctx.py b/flask/ctx.py index 90ea50db..5ceb1750 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -60,6 +60,7 @@ class _RequestContext(object): # exception happened. This will allow the debugger to still # access the request object in the interactive shell. Furthermore # the context can be force kept alive for the test client. + # See flask.testing for how this works. if not self.request.environ.get('flask._preserve_context') and \ (tb is None or not self.app.debug): self.pop() diff --git a/flask/module.py b/flask/module.py index 7bebac4b..df2b5b40 100644 --- a/flask/module.py +++ b/flask/module.py @@ -30,7 +30,7 @@ def _register_module(module, static_path): if state.url_prefix: path = state.url_prefix + path state.app.add_url_rule(path + '/', - '%s.static' % module.name, + endpoint='%s.static' % module.name, view_func=module.send_static_file) return _register diff --git a/flask/testing.py b/flask/testing.py new file mode 100644 index 00000000..ee973e0a --- /dev/null +++ b/flask/testing.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +""" + flask.testing + ~~~~~~~~~~~~~ + + Implements test support helpers. This module is lazily imported + and usually not used in production environments. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from werkzeug import Client +from flask import _request_ctx_stack + + +class FlaskClient(Client): + + preserve_context = context_preserved = False + + def open(self, *args, **kwargs): + if self.context_preserved: + _request_ctx_stack.pop() + self.context_preserved = False + kwargs.setdefault('environ_overrides', {}) \ + ['flask._preserve_context'] = self.preserve_context + old = _request_ctx_stack.top + try: + return Client.open(self, *args, **kwargs) + finally: + self.context_preserved = _request_ctx_stack.top is not old + + def __enter__(self): + self.preserve_context = True + return self + + def __exit__(self, exc_type, exc_value, tb): + self.preserve_context = False + if self.context_preserved: + _request_ctx_stack.pop() From e5d82020384ae527c36424b52612bbe625db9268 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 5 Jul 2010 10:32:06 +0200 Subject: [PATCH 061/207] Added a docstring --- flask/testing.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flask/testing.py b/flask/testing.py index ee973e0a..fe7f8c2d 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -14,6 +14,11 @@ from flask import _request_ctx_stack class FlaskClient(Client): + """Works like a regular Werkzeug test client but has some + knowledge about how Flask works to defer the cleanup of the + request context stack to the end of a with body when used + in a with statement. + """ preserve_context = context_preserved = False From 77e2fbf249031a7adb7e745f15ad5645027b5994 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 5 Jul 2010 10:37:44 +0200 Subject: [PATCH 062/207] Added a separate logging module --- flask/app.py | 19 +++---------------- flask/logging.py | 42 ++++++++++++++++++++++++++++++++++++++++++ flask/testing.py | 1 + 3 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 flask/logging.py diff --git a/flask/app.py b/flask/app.py index dbc102f7..e5691edd 100644 --- a/flask/app.py +++ b/flask/app.py @@ -300,22 +300,9 @@ class Flask(_PackageBoundObject): with _logger_lock: if self._logger and self._logger.name == self.logger_name: return self._logger - from logging import getLogger, StreamHandler, Formatter, \ - Logger, DEBUG - class DebugLogger(Logger): - def getEffectiveLevel(x): - return DEBUG if self.debug else Logger.getEffectiveLevel(x) - class DebugHandler(StreamHandler): - def emit(x, record): - StreamHandler.emit(x, record) if self.debug else None - handler = DebugHandler() - handler.setLevel(DEBUG) - handler.setFormatter(Formatter(self.debug_log_format)) - logger = getLogger(self.logger_name) - logger.__class__ = DebugLogger - logger.addHandler(handler) - self._logger = logger - return logger + from flask.logging import create_logger + self._logger = rv = create_logger(self) + return rv def create_jinja_environment(self): """Creates the Jinja2 environment based on :attr:`jinja_options` diff --git a/flask/logging.py b/flask/logging.py new file mode 100644 index 00000000..4ccd8b1b --- /dev/null +++ b/flask/logging.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +""" + flask.logging + ~~~~~~~~~~~~~ + + Implements the logging support for Flask. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import absolute_import + +from logging import getLogger, StreamHandler, Formatter, Logger, DEBUG + + +def create_logger(app): + """Creates a logger for the given application. This logger works + similar to a regular Python logger but changes the effective logging + level based on the application's debug flag. Furthermore this + function also removes all attached handlers in case there was a + logger with the log name before. + """ + + class DebugLogger(Logger): + def getEffectiveLevel(x): + return DEBUG if app.debug else Logger.getEffectiveLevel(x) + + class DebugHandler(StreamHandler): + def emit(x, record): + StreamHandler.emit(x, record) if app.debug else None + + handler = DebugHandler() + handler.setLevel(DEBUG) + handler.setFormatter(Formatter(app.debug_log_format)) + logger = getLogger(app.logger_name) + # just in case that was not a new logger, get rid of all the handlers + # already attached to it. + del logger.handlers[:] + logger.__class__ = DebugLogger + logger.addHandler(handler) + return logger diff --git a/flask/testing.py b/flask/testing.py index fe7f8c2d..ee4bd28e 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -9,6 +9,7 @@ :copyright: (c) 2010 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ + from werkzeug import Client from flask import _request_ctx_stack From 56796f0f439a5c0e00d7b3d73a662a2a8e11e7c1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 5 Jul 2010 11:06:29 +0200 Subject: [PATCH 063/207] More doc changes regarding foreword --- docs/foreword.rst | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/foreword.rst b/docs/foreword.rst index 6b9d99b2..b43fe870 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -10,9 +10,10 @@ What does "micro" mean? To me, the "micro" in microframework refers not only to the simplicity and small size of the framework, but also to the typically limited complexity -and size of applications that are written with the framework. To be -approachable and concise, a microframework sacrifices a few features that -may be necessary in larger or more complex applications. +and size of applications that are written with the framework. Also the +fact that you can have an entire application in a single Python file. To +be approachable and concise, a microframework sacrifices a few features +that may be necessary in larger or more complex applications. For example, Flask uses thread-local objects internally so that you don't have to pass objects around from function to function within a request in @@ -72,13 +73,13 @@ So always keep security in mind when doing web development. Target Audience --------------- -Is Flask for you? If your application is small-ish and does not depend on -very complex database structures, Flask is the Framework for you. It was -designed from the ground up to be easy to use, and built on the firm -foundation of established principles, good intentions, and mature, widely -used libraries. Recent versions of Flask scale nicely within reasonable -bounds, and if you grow larger, you won't have any trouble adjusting Flask -for your new application size. +Is Flask for you? If your application is small or medium sized and does +not depend on very complex database structures, Flask is the Framework for +you. It was designed from the ground up to be easy to use, and built on +the firm foundation of established principles, good intentions, and +mature, widely used libraries. Recent versions of Flask scale nicely +within reasonable bounds, and if you grow larger, you won't have any +trouble adjusting Flask for your new application size. If you suddenly discover that your application grows larger than originally intended, head over to the :ref:`becomingbig` section to see From da514b398429653dbd368c6da48c9863d3c2632f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 10:42:36 +0200 Subject: [PATCH 064/207] Respect the domain for the session cookie. This fixes #79 --- flask/app.py | 7 +++++-- tests/flask_tests.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/flask/app.py b/flask/app.py index e5691edd..0bdce818 100644 --- a/flask/app.py +++ b/flask/app.py @@ -420,11 +420,14 @@ class Flask(_PackageBoundObject): object) :param response: an instance of :attr:`response_class` """ - expires = None + expires = domain = None if session.permanent: expires = datetime.utcnow() + self.permanent_session_lifetime + if self.config['SERVER_NAME'] is not None: + domain = '.' + self.config['SERVER_NAME'] session.save_cookie(response, self.session_cookie_name, - expires=expires, httponly=True) + expires=expires, httponly=True, + domain=domain) def register_module(self, module, **options): """Registers a module with this application. The keyword argument diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 129ec3b2..1da3b23c 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -172,6 +172,20 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert c.post('/set', data={'value': '42'}).data == 'value set' assert c.get('/get').data == '42' + def test_session_using_server_name(self): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='foo', + SERVER_NAME='example.com' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://example.com/') + assert 'domain=.example.com' in rv.headers['set-cookie'].lower() + assert 'httponly' in rv.headers['set-cookie'].lower() + def test_missing_session(self): app = flask.Flask(__name__) def expect_exception(f, *args, **kwargs): From 60e143ecb63df6d820dacb1ac6416e0c73714524 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 10:43:38 +0200 Subject: [PATCH 065/207] Documented change regarding the session cookie domain policy in the CHANGES --- CHANGES | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 10ed8e9c..1fdc0d86 100644 --- a/CHANGES +++ b/CHANGES @@ -10,7 +10,8 @@ Codename to be decided, release date to be announced. - fixed a bug with subdomains that was caused by the inability to specify the server name. The server name can now be set with - the `SERVER_NAME` config key. + the `SERVER_NAME` config key. This key is now also used to set + the session cookie cross-subdomain wide. - autoescaping is no longer active for all templates. Instead it is only active for ``.html``, ``.htm``, ``.xml`` and ``.xhtml``. Inside templates this behaviour can be changed with the From 77743a5f15ddbc7d826abd4f189217bef88991b6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 12:33:39 +0200 Subject: [PATCH 066/207] Some documentation improvements --- docs/patterns/packages.rst | 46 ++++++++++++++++++++++++++++++++++++++ flask/module.py | 4 +++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index db5b00a3..cb372f2e 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -197,3 +197,49 @@ did in the example above, or we just use the function name:: @frontend.route('/') def index(): return "I'm the index" + +.. _modules-and-resources: + +Modules and Resources +--------------------- + +.. versionadded:: 0.5 + +If a module is located inside an actual Python package it may contain +static files and templates. Imagine you have an application like this:: + + + /yourapplication + __init__.py + /apps + /frontend + __init__.py + views.py + /static + style.css + /templates + index.html + about.html + ... + /admin + __init__.py + views.py + /static + style.css + /templates + list_items.html + show_item.html + ... + +The static folders automatically become exposed as URLs. For example if +the `admin` module is exported with an URL prefix of ``/admin`` you can +access the style css from its static folder by going to +``/admin/static/style.css``. The URL endpoint for the static files of the +admin would be ``'admin.static'``, similar to how you refer to the regular +static folder of the whole application as ``'static'``. + +If you want to refer to the templates you just have to prefix it with the +name of the module. So for the admin it would be +``render_template('admin/list_items.html')`` and so on. It is not +possible to refer to templates without the prefixed modlue name. This is +explicit unlike URL rules. diff --git a/flask/module.py b/flask/module.py index df2b5b40..b30020c5 100644 --- a/flask/module.py +++ b/flask/module.py @@ -90,7 +90,9 @@ class Module(_PackageBoundObject): :ref:`working-with-modules` section. .. versionadded:: 0.5 - The `static_path` parameter was added. + The `static_path` parameter was added and it's now possible for + modules to refer to their own templates and static files. See + :ref:`modules-and-resources` for more information. :param import_name: the name of the Python package or module implementing this :class:`Module`. From b7c0e564d4c8ddd82550f2c2958672558c1c657b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 14:42:43 +0200 Subject: [PATCH 067/207] Added a chapter on configuration options --- docs/config.rst | 67 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/docs/config.rst b/docs/config.rst index fabf6dc4..522d5d73 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -132,3 +132,70 @@ experience: 2. Do not write code that needs the configuration at import time. If you limit yourself to request-only accesses to the configuration you can reconfigure the object later on as needed. + + +Development / Production +------------------------ + +Most applications need more than one configuration. There will at least +be a separate configuration for a production server and one used during +development. The easiest way to handle this is to use a default +configuration that is always loaded and part of version control, and a +separate configuration that overrides the values as necessary as mentioned +in the example above:: + + app = Flask(__name__) + app.config.from_object('yourapplication.default_settings') + app.config.from_envvar('YOURAPPLICATION_SETTINGS') + +Then you just have to add a separate `config.py` file and export +``YOURAPPLICATION_SETTINGS=/path/to/config.py`` and you are done. However +there are alternative ways as well. For example you could use imports or +subclassing. + +What is very popular in the Django world is to make the import explicit in +the config file by adding an ``from yourapplication.default_settings +import *`` to the top of the file and then overriding the changes by hand. +You could also inspect an environment variable like +``YOURAPPLICATION_MODE`` and set that to `production`, `development` etc +and import different hardcoded files based on that. + +An interesting pattern is also to use classes and inheritance for +configuration:: + + class Config(object): + DEBUG = False + TESTING = False + DATABASE_URI = 'sqlite://:memory:' + + class ProductionConfig(Config): + DATABASE_URI = 'mysql://user@localhost/foo' + + class DevelopmentConfig(Config): + DEBUG = True + + class TestinConfig(Config): + TESTING = True + +To enable such a config you just have to call into +:meth:`~flask.Config.from_object`:: + + app.config.from_object('configmodule.ProductionConfig') + +There are many different ways and it's up to you how you want to manage +your configuration files. However here a list of good recommendations:: + +- keep a default configuration in version control. Either populate the + config with this default configuration or import it in your own + configuration files before overriding values. +- use an environment variable to switch between the configurations. + This can be done from outside the Python interpreter and makes + development and deployment much easier because you can quickly and + easily switch between different configs without having to touch the + code at all. If you are working often on different projects you can + even create your own script for sourcing that activates a virtualenv + and exports the development configuration for you. +- Use a tool like `fabric`_ in production to push code and + configurations sepearately to the production server(s). + +.. _fabric: http://fabfile.org/ From 34fcd19306dca6359d351bb987b686ca12bb2ab6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 16:07:13 +0200 Subject: [PATCH 068/207] Added chapter about fabric based deployments --- docs/config.rst | 5 +- docs/deploying/mod_wsgi.rst | 15 +++ docs/patterns/distribute.rst | 4 + docs/patterns/fabric.rst | 196 +++++++++++++++++++++++++++++++++++ docs/patterns/index.rst | 1 + 5 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 docs/patterns/fabric.rst diff --git a/docs/config.rst b/docs/config.rst index 522d5d73..0e8a24cd 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -183,7 +183,7 @@ To enable such a config you just have to call into app.config.from_object('configmodule.ProductionConfig') There are many different ways and it's up to you how you want to manage -your configuration files. However here a list of good recommendations:: +your configuration files. However here a list of good recommendations: - keep a default configuration in version control. Either populate the config with this default configuration or import it in your own @@ -196,6 +196,7 @@ your configuration files. However here a list of good recommendations:: even create your own script for sourcing that activates a virtualenv and exports the development configuration for you. - Use a tool like `fabric`_ in production to push code and - configurations sepearately to the production server(s). + configurations sepearately to the production server(s). For some + details about how to do that, head over to the :ref:`deploy` pattern. .. _fabric: http://fabfile.org/ diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index b2d25f5c..5b19f1d5 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -1,3 +1,5 @@ +.. _mod_wsgi-deployment: + mod_wsgi (Apache) ================= @@ -134,6 +136,19 @@ If your application does not run, follow this guide to troubleshoot: filename is used to locate the resources and for symlinks the wrong filename is picked up. +Support for Automatic Reloading +------------------------------- + +To help deployment tools you can activate support for automatic reloading. +Whenever something changes the `.wsgi` file, `mod_wsgi` will reload all +the daemon processes for us. + +For that, just add the following directive to your `Directory` section: + +.. sourcecode:: apache + + WSGIScriptReloading On + Working with Virtual Environments --------------------------------- diff --git a/docs/patterns/distribute.rst b/docs/patterns/distribute.rst index a3217f6e..b6f6a5ef 100644 --- a/docs/patterns/distribute.rst +++ b/docs/patterns/distribute.rst @@ -33,6 +33,10 @@ not supported by `distribute`_ so we will not bother with it. If you have not yet converted your application into a package, head over to the :ref:`larger-applications` pattern to see how this can be done. +A working deployment with distribute is the first step into more complex +and more automated deployment scenarios. If you want to fully automate +the process, also read the :ref:`fabric-deployment` chapter. + Basic Setup Script ------------------ diff --git a/docs/patterns/fabric.rst b/docs/patterns/fabric.rst new file mode 100644 index 00000000..5f306cec --- /dev/null +++ b/docs/patterns/fabric.rst @@ -0,0 +1,196 @@ +.. _fabric-deployment: + +Deploying with Fabric +===================== + +`Fabric`_ is a tool for Python similar to Makefiles but with the ability +to execute commands on a remote server. In combination with a properly +set up Python package (:ref:`larger-applications`) and a good concept for +configurations (:ref:`config`) it is very easy to deploy Flask +applications to external servers. + +Before we get started, here a quick checklist of things we have to ensure +upfront: + +- Fabric 1.0 has to be installed locally. This tutorial assumes the + latest version of Fabric. +- The application already has to be a package and requires a working + `setup.py` file (:ref:`distribute-deployment`). +- In the following example we are using `mod_wsgi` for the remote + servers. You can of course use your own favourite server there, but + for this example we chose Apache + `mod_wsgi` because it's very easy + to setup and has a simple way to reload applications without root + access. + +Creating the first Fabfile +-------------------------- + +A fabfile is what controls what Fabric executes. It is named `fabfile.py` +and executed by the `fab` command. All the functions defined in that file +will show up as `fab` subcommands. They are executed on one or more +hosts. These hosts can be defined either in the fabfile or on the command +line. In this case we will add them to the fabfile. + +This is a basic first example that has the ability to upload the current +sourcecode to the server and install it into a already existing +virtual environment:: + + from fabric.api import * + + # the user to use for the remote commands + env.user = 'appuser' + # the servers where the commands are executed + env.hosts = ['server1.example.com', 'server2.example.com'] + + def pack(): + # create a new source distribution as tarball + local('python setup.py sdist --formats=gztar', capture=False) + + def deploy(): + # figure out the release name and version + dist = local('python setup.py --fullname').strip() + # upload the source tarball to the temporary folder on the server + put('sdist/%s.tar.gz' % dist, '/tmp/') + # create a place where we can unzip the tarball, then enter + # that directory and unzip it + run('mkdir yourapplication') + with cd('/tmp/yourapplication'): + run('tar xzf /tmp/yourapplication.tar.gz') + # now setup the package with our virtual environment's + # python interpreter + run('/var/www/yourapplication/env/bin/python setup.py install') + # now that all is set up, delete the folder again + run('rm -rf /tmp/yourapplication /tmp/yourapplication.tar.gz') + # and finally touch the .wsgi file so that mod_wsgi triggers + # a reload of the application + run('touch /var/www/yourapplication.wsgi') + +The example above is well documented and should be straightforward. Here +a recap of the most common commands fabric provides: + +- `run` - executes a command on a remote server +- `local` - executes a command on the local machine +- `put` - uploads a file to the remote server +- `cd` - changes the directory on the serverside. This has to be used + in combination with the `with` statement. + +Running Fabfiles +---------------- + +Now how do you execute that fabfile? You use the `fab` command. To +deploy the current version of the code on the remote server you would use +this command:: + + $ fab pack deploy + +However this requires that our server already has the +``/var/www/yourapplication`` folder created and +``/var/www/yourapplication/env`` to be a virtual environment. Furthermore +are we not creating the configuration or `.wsgi` file on the server. So +how do we bootstrap a new server into our infrastructure? + +This now depends on the number of servers we want to set up. If we just +have one application server (which the majority of applications will +have), creating a command in the fabfile for this is overkill. But +obviously you can do that. In that case you would probably call it +`setup` or `bootstrap` and then pass the servername explicitly on the +command line:: + + $ fab -H newserver.example.com bootstrap + +To setup a new server you would roughly do these steps: + +1. Create the directory structure in ``/var/www``:: + + $ mkdir /var/www/yourapplication + $ cd /var/www/yourapplication + $ virtualenv --distribute env + +2. Upload a new `application.wsgi` file to the server and the + configuration file for the application (eg: `application.cfg`) + +3. Create a new Apache config for `yourapplication` and activate it. + Make sure to activate watching for changes of the `.wsgi` file so + that we can automatically reload the application by touching it. + (See :ref:`mod_wsgi-deployment` for more information) + +So now the question is, where do the `application.wsgi` and +`application.cfg` files come from? + +The WSGI File +------------- + +The WSGI file has to import the application and also to set an environment +variable so that the application knows where to look for the config. This +is a short example that does exactly that:: + + import os + os.environ['YOURAPPLICATION_CONFIG'] = '/var/www/yourapplication/application.cfg' + from yourapplication import app + +The application itself then has to initialize itself like this to look for +the config at that environment variable:: + + app = Flask(__name__) + app.config.from_object('yourapplication.default_config') + app.config.from_envvar('YOURAPPLICATION_CONFIG') + +This approach is explained in detail in the :ref:`config` section of the +documentation. + +The Configuration File +---------------------- + +Now as mentioned above, the application will find the correct +configuration file by looking up the `YOURAPPLICATION_CONFIG` environment +variable. So we have to put the configuration in a place where the +application will able to find it. Configuration files have the unfriendly +quality of being different on all computers, so you do not version them +usually. + +A popular approach is to store configuration files for different servers +in a separate version control repository and check them out on all +servers. Then symlink the file that is active for the server into the +location where it's expected (eg: ``/var/www/yourapplication``). + +Either way, in our case here we only expect one or two servers and we can +upload them ahead of time by hand. + +First Deployment +---------------- + +Now we can do our first deployment. We have set up the servers so that +they have their virtual environments and activated apache configs. Now we +can pack up the application and deploy it:: + + $ fab pack deploy + +Fabric will now connect to all servers and run the commands as written +down in the fabfile. First it will execute pack so that we have our +tarball ready and then it will execute deploy and upload the source code +to all servers and install it there. Thanks to the `setup.py` file we +will automatically pull in the required libraries into our virtual +environment. + +Next Steps +---------- + +From that point onwards there is so much that can be done to make +deployment actually fun: + +- Create a `bootstrap` command that initializes new servers. It could + initialize a new virtual environment, setup apache appropriately etc. +- Put configuration files into a separate version control repository + and symlink the active configs into place. +- You could also put your application code into a repository and check + out the latest version on the server and then install. That way you + can also easily go back to older versions. +- hook in testing functionality so that you can deploy to an external + server and run the testsuite. + +Working with Fabric is fun and you will notice that it's quite magical to +type ``fab deploy`` and see your application being deployed automatically +to one or more remote servers. + + +.. _Fabric: http://fabfile.org/ diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 23b20dc5..21f3a562 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -19,6 +19,7 @@ Snippet Archives `_. packages appfactories distribute + fabric sqlite3 sqlalchemy fileuploads From 5dd4e9f318e3bb27967d86f42be64fa839967145 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 16:10:49 +0200 Subject: [PATCH 069/207] Fixed a typo --- docs/patterns/fabric.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/fabric.rst b/docs/patterns/fabric.rst index 5f306cec..de23ffa8 100644 --- a/docs/patterns/fabric.rst +++ b/docs/patterns/fabric.rst @@ -50,7 +50,7 @@ virtual environment:: # figure out the release name and version dist = local('python setup.py --fullname').strip() # upload the source tarball to the temporary folder on the server - put('sdist/%s.tar.gz' % dist, '/tmp/') + put('sdist/%s.tar.gz' % dist, '/tmp/yourapplication.tar.gz') # create a place where we can unzip the tarball, then enter # that directory and unzip it run('mkdir yourapplication') From 4c937be2524de0fddc2d2f7f39b09677497260aa Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 16:25:44 +0200 Subject: [PATCH 070/207] Preparing for release --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 1fdc0d86..68b42d94 100644 --- a/CHANGES +++ b/CHANGES @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Flask release. Version 0.5 ----------- -Codename to be decided, release date to be announced. +Released on July 6th 2010, codename Calvados. - fixed a bug with subdomains that was caused by the inability to specify the server name. The server name can now be set with From 3f7ab1797730b8d07497744878d109baa9b235a5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 16:28:47 +0200 Subject: [PATCH 071/207] HEAD is 0.6-dev --- CHANGES | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 68b42d94..0e58331a 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,11 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.6 +----------- + +Release date to be announced, codename to be decided. + Version 0.5 ----------- diff --git a/setup.py b/setup.py index dd3561da..b97a33f7 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ def run_tests(): setup( name='Flask', - version='0.5', + version='0.6', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From 98ea6579c80fc7754eef433e3d6a0f232b8be75a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 19:10:08 +0200 Subject: [PATCH 072/207] Preparing for a 0.5.1 bugfix release --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dd3561da..6e7005bf 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ def run_tests(): setup( name='Flask', - version='0.5', + version='0.5.1', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From 596ae0ced530adef7021c134b3b89e74154d7eb6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 19:11:51 +0200 Subject: [PATCH 073/207] Updated changelog --- CHANGES | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 68b42d94..e554c701 100644 --- a/CHANGES +++ b/CHANGES @@ -3,10 +3,18 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.5.1 +------------- + +Bugfix Release, released on July 6th 2010 + +- fixes an issue with template loading for modules + + Version 0.5 ----------- -Released on July 6th 2010, codename Calvados. +Released on July 6th 2010, codename Calvados - fixed a bug with subdomains that was caused by the inability to specify the server name. The server name can now be set with @@ -48,7 +56,7 @@ Released on June 18th 2010, codename Rakia Version 0.3.1 ------------- -Bugfix release, released May 28th 2010 +Bugfix release, released on May 28th 2010 - fixed a error reporting bug with :meth:`flask.Config.from_envvar` - removed some unused code from flask From 0a93c552cc3318396e44c6ab1282190898ce0012 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 19:24:50 +0200 Subject: [PATCH 074/207] Fixed a template lookup error --- CHANGES | 3 ++- flask/templating.py | 22 ++++++++++++---------- tests/flask_tests.py | 12 ++++++++++++ 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/CHANGES b/CHANGES index e554c701..103a66f3 100644 --- a/CHANGES +++ b/CHANGES @@ -8,7 +8,8 @@ Version 0.5.1 Bugfix Release, released on July 6th 2010 -- fixes an issue with template loading for modules +- fixes an issue with template loading from directories when modules + where used. Version 0.5 diff --git a/flask/templating.py b/flask/templating.py index d1e75959..06d8be04 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -34,21 +34,23 @@ class _DispatchingJinjaLoader(BaseLoader): self.app = app def get_source(self, environment, template): - name = template - loader = None try: module, name = template.split('/', 1) loader = self.app.modules[module].jinja_loader + if loader is None: + raise ValueError() except (ValueError, KeyError): - pass - if loader is None: loader = self.app.jinja_loader - try: - return loader.get_source(environment, name) - except TemplateNotFound: - # re-raise the exception with the correct fileame here. - # (the one that includes the prefix) - raise TemplateNotFound(template) + if loader is not None: + return loader.get_source(environment, template) + else: + try: + return loader.get_source(environment, name) + except TemplateNotFound: + pass + # raise the exception with the correct fileame here. + # (the one that includes the prefix) + raise TemplateNotFound(template) def list_templates(self): result = self.app.jinja_loader.list_templates() diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 1da3b23c..5b4f34ef 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -21,6 +21,7 @@ from contextlib import contextmanager from datetime import datetime from werkzeug import parse_date, parse_options_header from werkzeug.exceptions import NotFound +from jinja2 import TemplateNotFound from cStringIO import StringIO example_path = os.path.join(os.path.dirname(__file__), '..', 'examples') @@ -662,6 +663,17 @@ class ModuleTestCase(unittest.TestCase): assert flask.url_for('admin.static', filename='test.txt') \ == '/admin/static/test.txt' + with app.test_request_context(): + try: + flask.render_template('missing.html') + except TemplateNotFound, e: + assert e.name == 'missing.html' + else: + assert 0, 'expected exception' + + with flask.Flask(__name__).test_request_context(): + assert flask.render_template('nested/nested.txt') == 'I\'m nested' + def test_safe_access(self): from moduleapp import app From 16e4d5a6553ff9c89a61c44ddec7da21126b631a Mon Sep 17 00:00:00 2001 From: florentx Date: Wed, 7 Jul 2010 02:07:11 +0800 Subject: [PATCH 075/207] Various typos. --- docs/patterns/packages.rst | 2 +- flask/app.py | 5 ++--- flask/logging.py | 2 +- flask/module.py | 2 +- flask/session.py | 2 +- flask/templating.py | 2 +- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index cb372f2e..63d7d76e 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -241,5 +241,5 @@ static folder of the whole application as ``'static'``. If you want to refer to the templates you just have to prefix it with the name of the module. So for the admin it would be ``render_template('admin/list_items.html')`` and so on. It is not -possible to refer to templates without the prefixed modlue name. This is +possible to refer to templates without the prefixed module name. This is explicit unlike URL rules. diff --git a/flask/app.py b/flask/app.py index 0bdce818..d16613b7 100644 --- a/flask/app.py +++ b/flask/app.py @@ -31,7 +31,7 @@ from .globals import _request_ctx_stack, request from .session import Session, _NullSession from .module import _ModuleSetupState from .templating import _DispatchingJinjaLoader, \ - _default_template_ctx_processor + _default_template_ctx_processor # a lock used for logger initialization _logger_lock = Lock() @@ -426,8 +426,7 @@ class Flask(_PackageBoundObject): if self.config['SERVER_NAME'] is not None: domain = '.' + self.config['SERVER_NAME'] session.save_cookie(response, self.session_cookie_name, - expires=expires, httponly=True, - domain=domain) + expires=expires, httponly=True, domain=domain) def register_module(self, module, **options): """Registers a module with this application. The keyword argument diff --git a/flask/logging.py b/flask/logging.py index 4ccd8b1b..29caadce 100644 --- a/flask/logging.py +++ b/flask/logging.py @@ -11,7 +11,7 @@ from __future__ import absolute_import -from logging import getLogger, StreamHandler, Formatter, Logger, DEBUG +from logging import getLogger, StreamHandler, Formatter, Logger, DEBUG def create_logger(app): diff --git a/flask/module.py b/flask/module.py index b30020c5..8935359e 100644 --- a/flask/module.py +++ b/flask/module.py @@ -15,7 +15,7 @@ from .helpers import _PackageBoundObject def _register_module(module, static_path): """Internal helper function that returns a function for recording that registers the `send_static_file` function for the module on - the application of necessary. It also registers the module on + the application if necessary. It also registers the module on the application. """ def _register(state): diff --git a/flask/session.py b/flask/session.py index 324fc98d..df2d8773 100644 --- a/flask/session.py +++ b/flask/session.py @@ -37,7 +37,7 @@ class _NullSession(Session): def _fail(self, *args, **kwargs): raise RuntimeError('the session is unavailable because no secret ' 'key was set. Set the secret_key on the ' - 'application to something unique and secret') + 'application to something unique and secret.') __setitem__ = __delitem__ = clear = pop = popitem = \ update = setdefault = _fail del _fail diff --git a/flask/templating.py b/flask/templating.py index 06d8be04..23c55c2b 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -48,7 +48,7 @@ class _DispatchingJinjaLoader(BaseLoader): return loader.get_source(environment, name) except TemplateNotFound: pass - # raise the exception with the correct fileame here. + # raise the exception with the correct filename here. # (the one that includes the prefix) raise TemplateNotFound(template) From f1a6fbe250ccf5407b45a398d05bf63bc99c3105 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 20:28:48 +0200 Subject: [PATCH 076/207] send_from_directory > SharedDataMiddleware --- docs/patterns/fileuploads.rst | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst index 1174ebfc..f4a9a1db 100644 --- a/docs/patterns/fileuploads.rst +++ b/docs/patterns/fileuploads.rst @@ -28,8 +28,6 @@ bootstrapping code for our application:: ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif']) app = Flask(__name__) - app.add_url_rule('/uploads/', 'uploaded_file', - build_only=True) So first we need a couple of imports. Most should be straightforward, the :func:`werkzeug.secure_filename` is explained a little bit later. The @@ -100,14 +98,23 @@ before storing it directly on the filesystem. >>> secure_filename('../../../../home/username/.bashrc') 'home_username_.bashrc' -Now if we run that application, you will notice that uploading works, but -you won't actually see that uploaded file. Well, you would have to -configure the server to serve that file for you. This is not handy for -development situations or when you are just too lazy to properly set up -the server. Would be nice to have the files still be available in that -situation, and that is really easy to do, just hook in a middleware:: +Now one last thing is missing: the serving of the uploaded files. As of +Flask 0.5 we can use a function that does that for us:: + + from flask import send_from_directory + + @app.route('/uploads/') + def uploaded_file(filename): + return send_from_directory(app.config['UPLOAD_FOLDER'], + filename) + +Alternatively you can register `uploaded_file` as `build_only` rule and +use the :class:`~werkzeug.SharedDataMiddleware`. This also works with +older versions of Flask:: from werkzeug import SharedDataMiddleware + app.add_url_rule('/uploads/', 'uploaded_file', + build_only=True) app.wsgi_app = SharedDataMiddleware(app.wsgi_app, { '/uploads': UPLOAD_FOLDER }) From 0198313a22a98361947fceb8544c355be676f784 Mon Sep 17 00:00:00 2001 From: Thomas Schranz Date: Thu, 8 Jul 2010 00:10:29 +0800 Subject: [PATCH 077/207] added workaround for json on Google AppEngine Google AppEngine unfortunately does not offer json/simplejson where we would expect it to be, but since they do offer django and django comes with simplejson as a frozen dependency, we can import it from there. prior art: http://github.com/facebook/python-sdk/blob/master/src/facebook.py#L50 --- flask/helpers.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 560492c6..420bfab8 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -26,7 +26,12 @@ except ImportError: try: import json except ImportError: - json_available = False + try: + # Google Appengine offers simplejson via django + from django.utils import simplejson as json + except ImportError: + json_available = False + from werkzeug import Headers, wrap_file, is_resource_modified, cached_property from werkzeug.exceptions import NotFound From f3b6d94bf732a913aaf2005ad2656ee31ef8f6c6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 7 Jul 2010 09:19:41 -0700 Subject: [PATCH 078/207] added Thomas Schranz to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index e20a3735..c2415977 100644 --- a/AUTHORS +++ b/AUTHORS @@ -24,4 +24,5 @@ Patches and Suggestions - Sebastien Estienne - Simon Sapin - Stephane Wirtel +- Thomas Schranz - Zhao Xiaohong From d12d73263f5d1664a65a42f71f19158b7e07ef2c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 12 Jul 2010 18:04:10 +0200 Subject: [PATCH 079/207] Reverse order of execution of post-request handlers. This fixes #82 --- CHANGES | 3 +++ docs/upgrading.rst | 11 +++++++++++ flask/app.py | 8 ++++++-- tests/flask_tests.py | 24 ++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 840d1a3d..c09b9e04 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,9 @@ Version 0.6 Release date to be announced, codename to be decided. +- after request functions are now called in reverse order of + registration. + Version 0.5.1 ------------- diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 7ee35176..f3a7a27d 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -19,6 +19,17 @@ installation, make sure to pass it the ``-U`` parameter:: $ easy_install -U Flask +Version 0.6 +----------- + +Flask 0.6 comes with a backwards incompatible change which affects the +order of after-request handlers. Previously they were called in the order +of the registration, now they are called in reverse order. This change +was made so that Flask behaves more like people expected it to work and +how other systems handle request pre- and postprocessing. If you +dependend on the order of execution of post-request functions, be sure to +change the order. + Version 0.5 ----------- diff --git a/flask/app.py b/flask/app.py index d16613b7..49852e5e 100644 --- a/flask/app.py +++ b/flask/app.py @@ -707,6 +707,10 @@ class Flask(_PackageBoundObject): before it's sent to the WSGI server. By default this will call all the :meth:`after_request` decorated functions. + .. versionchanged:: 0.5 + As of Flask 0.5 the functions registered for after request + execution are called in reverse order of registration. + :param response: a :attr:`response_class` object. :return: a new response object or the same, has to be an instance of :attr:`response_class`. @@ -717,9 +721,9 @@ class Flask(_PackageBoundObject): self.save_session(ctx.session, response) funcs = () if mod and mod in self.after_request_funcs: - funcs = chain(funcs, self.after_request_funcs[mod]) + funcs = reversed(self.after_request_funcs[mod]) if None in self.after_request_funcs: - funcs = chain(funcs, self.after_request_funcs[None]) + funcs = chain(funcs, reversed(self.after_request_funcs[None])) for handler in funcs: response = handler(response) return response diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 5b4f34ef..380ecdad 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -318,6 +318,30 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert 'Internal Server Error' in rv.data assert len(called) == 1 + def test_before_after_request_order(self): + called = [] + app = flask.Flask(__name__) + @app.before_request + def before1(): + called.append(1) + @app.before_request + def before2(): + called.append(2) + @app.after_request + def after1(response): + called.append(4) + return response + @app.after_request + def after1(response): + called.append(3) + return response + @app.route('/') + def index(): + return '42' + rv = app.test_client().get('/') + assert rv.data == '42' + assert called == [1, 2, 3, 4] + def test_error_handling(self): app = flask.Flask(__name__) @app.errorhandler(404) From 5e1b1030e8edecb0c9652f2a406933c049ea2397 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 12 Jul 2010 23:04:24 +0200 Subject: [PATCH 080/207] Added support for automagic OPTIONS --- CHANGES | 3 +++ docs/quickstart.rst | 8 +++++++- flask/app.py | 37 +++++++++++++++++++++++++++++++------ flask/ctx.py | 5 +++-- flask/helpers.py | 1 + flask/wrappers.py | 21 ++++++++++++++++----- tests/flask_tests.py | 17 +++++++++++++---- 7 files changed, 74 insertions(+), 18 deletions(-) diff --git a/CHANGES b/CHANGES index c09b9e04..0c8f965e 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,9 @@ Release date to be announced, codename to be decided. - after request functions are now called in reverse order of registration. +- OPTIONS is now automatically implemented by Flask unless the + application explictly adds 'OPTIONS' as method to the URL rule. + In this case no automatic OPTIONS handling kicks in. Version 0.5.1 ------------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index e1fdce51..4e98f858 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -269,7 +269,8 @@ If `GET` is present, `HEAD` will be added automatically for you. You don't have to deal with that. It will also make sure that `HEAD` requests are handled like the `HTTP RFC`_ (the document describing the HTTP protocol) demands, so you can completely ignore that part of the HTTP -specification. +specification. Likewise as of Flask 0.6, `OPTIONS` is implemented for you +as well automatically. You have no idea what an HTTP method is? Worry not, here quick introduction in HTTP methods and why they matter: @@ -310,6 +311,11 @@ very common: `DELETE` Remove the information that the given location. +`OPTIONS` + Provides a quick way for a requesting client to figure out which + methods are supported by this URL. Starting with Flask 0.6, this + is implemented for you automatically. + Now the interesting part is that in HTML4 and XHTML1, the only methods a form might submit to the server are `GET` and `POST`. But with JavaScript and future HTML standards you can use other methods as well. Furthermore diff --git a/flask/app.py b/flask/app.py index 49852e5e..02f22aa3 100644 --- a/flask/app.py +++ b/flask/app.py @@ -464,6 +464,9 @@ class Flask(_PackageBoundObject): .. versionchanged:: 0.2 `view_func` parameter added. + .. versionchanged:: 0.6 + `OPTIONS` is added automatically as method. + :param rule: the URL rule as string :param endpoint: the endpoint for the registered URL rule. Flask itself assumes the name of the view function as @@ -471,15 +474,27 @@ class Flask(_PackageBoundObject): :param view_func: the function to call when serving a request to the provided endpoint :param options: the options to be forwarded to the underlying - :class:`~werkzeug.routing.Rule` object + :class:`~werkzeug.routing.Rule` object. A change + to Werkzeug is handling of method options. methods + is a list of methods this rule should be limited + to (`GET`, `POST` etc.). By default a rule + just listens for `GET` (and implicitly `HEAD`). + Starting with Flask 0.6, `OPTIONS` is implicitly + added and handled by the standard request handling. """ if endpoint is None: assert view_func is not None, 'expected view func if endpoint ' \ 'is not provided.' endpoint = view_func.__name__ options['endpoint'] = endpoint - options.setdefault('methods', ('GET',)) - self.url_map.add(Rule(rule, **options)) + methods = options.pop('methods', ('GET',)) + provide_automatic_options = False + if 'OPTIONS' not in methods: + methods = tuple(methods) + ('OPTIONS',) + provide_automatic_options = True + rule = Rule(rule, methods=methods, **options) + rule.provide_automatic_options = provide_automatic_options + self.url_map.add(rule) if view_func is not None: self.view_functions[endpoint] = view_func @@ -539,8 +554,10 @@ class Flask(_PackageBoundObject): :param rule: the URL rule as string :param methods: a list of methods this rule should be limited - to (``GET``, ``POST`` etc.). By default a rule - just listens for ``GET`` (and implicitly ``HEAD``). + to (`GET`, `POST` etc.). By default a rule + just listens for `GET` (and implicitly `HEAD`). + Starting with Flask 0.6, `OPTIONS` is implicitly + added and handled by the standard request handling. :param subdomain: specifies the rule for the subdomain in case subdomain matching is in use. :param strict_slashes: can be used to disable the strict slashes @@ -650,7 +667,15 @@ class Flask(_PackageBoundObject): try: if req.routing_exception is not None: raise req.routing_exception - return self.view_functions[req.endpoint](**req.view_args) + rule = req.url_rule + # if we provide automatic options for this URL and the + # request came with the OPTIONS method, reply automatically + if rule.provide_automatic_options and req.method == 'OPTIONS': + rv = self.response_class() + rv.allow.update(rule.methods) + return rv + # otherwise dispatch to the handler for that endpoint + return self.view_functions[rule.endpoint](**req.view_args) except HTTPException, e: return self.handle_http_exception(e) diff --git a/flask/ctx.py b/flask/ctx.py index 5ceb1750..854503af 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -38,8 +38,9 @@ class _RequestContext(object): self.flashes = None try: - self.request.endpoint, self.request.view_args = \ - self.url_adapter.match() + url_rule, self.request.view_args = \ + self.url_adapter.match(return_rule=True) + self.request.url_rule = url_rule except HTTPException, e: self.request.routing_exception = e diff --git a/flask/helpers.py b/flask/helpers.py index 420bfab8..cf00528e 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -15,6 +15,7 @@ import posixpath import mimetypes from time import time from zlib import adler32 +from functools import wraps # try to load the best simplejson implementation available. If JSON # is not installed, we add a failing class. diff --git a/flask/wrappers.py b/flask/wrappers.py index 1dcf23d1..200e7caf 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -24,11 +24,12 @@ class Request(RequestBase): :attr:`~flask.Flask.request_class` to your subclass. """ - #: the endpoint that matched the request. This in combination with - #: :attr:`view_args` can be used to reconstruct the same or a - #: modified URL. If an exception happened when matching, this will - #: be `None`. - endpoint = None + #: the internal URL rule that matched the request. This can be + #: useful to inspect which methods are allowed for the URL from + #: a before/after handler (``request.url_rule.methods``) etc. + #: + #: .. versionadded:: 0.6 + url_rule = None #: a dict of view arguments that matched the request. If an exception #: happened when matching, this will be `None`. @@ -40,6 +41,16 @@ class Request(RequestBase): #: something similar. routing_exception = None + @property + def endpoint(self): + """The endpoint that matched the request. This in combination with + :attr:`view_args` can be used to reconstruct the same or a + modified URL. If an exception happened when matching, this will + be `None`. + """ + if self.url_rule is not None: + return self.url_rule.endpoint + @property def module(self): """The name of the current module""" diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 380ecdad..6c16bbd4 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -111,6 +111,15 @@ class ContextTestCase(unittest.TestCase): class BasicFunctionalityTestCase(unittest.TestCase): + def test_options_work(self): + app = flask.Flask(__name__) + @app.route('/', methods=['GET', 'POST']) + def index(): + return 'Hello World' + rv = app.test_client().open('/', method='OPTIONS') + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] + assert rv.data == '' + def test_request_dispatching(self): app = flask.Flask(__name__) @app.route('/') @@ -124,7 +133,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert c.get('/').data == 'GET' rv = c.post('/') assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD'] + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] rv = c.head('/') assert rv.status_code == 200 assert not rv.data # head truncates @@ -132,7 +141,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert c.get('/more').data == 'GET' rv = c.delete('/more') assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'POST'] + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] def test_url_mapping(self): app = flask.Flask(__name__) @@ -148,7 +157,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert c.get('/').data == 'GET' rv = c.post('/') assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD'] + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] rv = c.head('/') assert rv.status_code == 200 assert not rv.data # head truncates @@ -156,7 +165,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert c.get('/more').data == 'GET' rv = c.delete('/more') assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'POST'] + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] def test_session(self): app = flask.Flask(__name__) From 6e52355eb303e1b47f7b83ddbb94ff432e2df139 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 12 Jul 2010 23:58:43 +0200 Subject: [PATCH 081/207] tiny performance improvement --- flask/wrappers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/wrappers.py b/flask/wrappers.py index 200e7caf..b0747564 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -54,8 +54,8 @@ class Request(RequestBase): @property def module(self): """The name of the current module""" - if self.endpoint and '.' in self.endpoint: - return self.endpoint.rsplit('.', 1)[0] + if self.url_rule and '.' in self.url_rule.endpoint: + return self.url_rule.endpoint.rsplit('.', 1)[0] @cached_property def json(self): From ed16ae2183ad44a7ba89aedd331a180455ed0836 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 13 Jul 2010 23:14:53 +0200 Subject: [PATCH 082/207] Always register URL rules. This fixes #81 --- CHANGES | 4 ++++ flask/app.py | 13 ++++++++----- flask/helpers.py | 4 +--- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/CHANGES b/CHANGES index 0c8f965e..db26bdc7 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,10 @@ Release date to be announced, codename to be decided. - OPTIONS is now automatically implemented by Flask unless the application explictly adds 'OPTIONS' as method to the URL rule. In this case no automatic OPTIONS handling kicks in. +- static rules are now even in place if there is no static folder + for the module. This was implemented to aid GAE which will + remove the static folder if it's part of a mapping in the .yml + file. Version 0.5.1 ------------- diff --git a/flask/app.py b/flask/app.py index 02f22aa3..a70a930d 100644 --- a/flask/app.py +++ b/flask/app.py @@ -271,11 +271,14 @@ class Flask(_PackageBoundObject): #: app.url_map.converters['list'] = ListConverter self.url_map = Map() - # if there is a static folder, register it for the application. - if self.has_static_folder: - self.add_url_rule(self.static_path + '/', - endpoint='static', - view_func=self.send_static_file) + # register the static folder for the application. Do that even + # if the folder does not exist. First of all it might be created + # while the server is running (usually happens during development) + # but also because google appengine stores static files somewhere + # else when mapped with the .yml file. + self.add_url_rule(self.static_path + '/', + endpoint='static', + view_func=self.send_static_file) #: The Jinja2 environment. It is created from the #: :attr:`jinja_options`. diff --git a/flask/helpers.py b/flask/helpers.py index cf00528e..99a001dc 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -366,9 +366,7 @@ class _PackageBoundObject(object): .. versionadded:: 0.5 """ - template_folder = os.path.join(self.root_path, 'templates') - if os.path.isdir(template_folder): - return FileSystemLoader(template_folder) + return FileSystemLoader(os.path.join(self.root_path, 'templates')) def send_static_file(self, filename): """Function used internally to send static files from the static From aa3d8398fd1a38b449b64fea4979f358f3c711e3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 13 Jul 2010 23:30:29 +0200 Subject: [PATCH 083/207] Config is now available in templates, context processors no longer override keys --- CHANGES | 4 ++++ docs/upgrading.rst | 8 ++++++++ flask/app.py | 11 ++++++++++- flask/templating.py | 1 + tests/flask_tests.py | 24 ++++++++++++++++++++++++ 5 files changed, 47 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index db26bdc7..0c04a91d 100644 --- a/CHANGES +++ b/CHANGES @@ -17,6 +17,10 @@ Release date to be announced, codename to be decided. for the module. This was implemented to aid GAE which will remove the static folder if it's part of a mapping in the .yml file. +- the :attr:`~flask.Flask.config` is now available in the templates + as `config`. +- context processors will no longer override values passed directly + to the render function. Version 0.5.1 ------------- diff --git a/docs/upgrading.rst b/docs/upgrading.rst index f3a7a27d..747fdb72 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -30,6 +30,14 @@ how other systems handle request pre- and postprocessing. If you dependend on the order of execution of post-request functions, be sure to change the order. +Another change that breaks backwards compatibility is that context +processors will no longer override values passed directly to the template +rendering function. If for example `request` is as variable passed +directly to the template, the default context processor will not override +it with the current request object. This makes it easier to extend +context processors later to inject additional variables without breaking +existing template not expecting them. + Version 0.5 ----------- diff --git a/flask/app.py b/flask/app.py index a70a930d..fd97bab4 100644 --- a/flask/app.py +++ b/flask/app.py @@ -343,7 +343,11 @@ class Flask(_PackageBoundObject): def update_template_context(self, context): """Update the template context with some commonly used variables. - This injects request, session and g into the template context. + This injects request, session, config and g into the template + context as well as everything template context processors want + to inject. Note that the as of Flask 0.6, the original values + in the context will not be overriden if a context processor + decides to return a value with the same key. :param context: the context as a dictionary that is updated in place to add extra variables. @@ -352,8 +356,13 @@ class Flask(_PackageBoundObject): mod = _request_ctx_stack.top.request.module if mod is not None and mod in self.template_context_processors: funcs = chain(funcs, self.template_context_processors[mod]) + orig_ctx = context.copy() for func in funcs: context.update(func()) + # make sure the original values win. This makes it possible to + # easier add new variables in context processors without breaking + # existing views. + context.update(orig_ctx) def run(self, host='127.0.0.1', port=5000, **options): """Runs the application on a local development server. If the diff --git a/flask/templating.py b/flask/templating.py index 23c55c2b..c55d4826 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -19,6 +19,7 @@ def _default_template_ctx_processor(): """ reqctx = _request_ctx_stack.top return dict( + config=reqctx.app.config, request=reqctx.request, session=reqctx.session, g=reqctx.g diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 6c16bbd4..533afd21 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -492,6 +492,30 @@ class TemplatingTestCase(unittest.TestCase): rv = app.test_client().get('/') assert rv.data == '

23|42' + def test_original_win(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.render_template_string('{{ config }}', config=42) + rv = app.test_client().get('/') + assert rv.data == '42' + + def test_standard_context(self): + app = flask.Flask(__name__) + app.secret_key = 'development key' + @app.route('/') + def index(): + flask.g.foo = 23 + flask.session['test'] = 'aha' + return flask.render_template_string(''' + {{ request.args.foo }} + {{ g.foo }} + {{ config.DEBUG }} + {{ session.test }} + ''') + rv = app.test_client().get('/?foo=42') + assert rv.data.split() == ['42', '23', 'False', 'aha'] + def test_escaping(self): text = '

Hello World!' app = flask.Flask(__name__) From 85ff63c32e7237280bff3293481a371fb3da180c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 13 Jul 2010 23:52:55 +0200 Subject: [PATCH 084/207] Emit correct date. In theory --- flask/helpers.py | 11 +++++++++++ tests/flask_tests.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 99a001dc..d76090c4 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -240,6 +240,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, :param conditional: set to `True` to enable conditional responses. :param cache_timeout: the timeout in seconds for the headers. """ + mtime = None if isinstance(filename_or_fp, basestring): filename = filename_or_fp file = None @@ -272,11 +273,21 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, else: if file is None: file = open(filename, 'rb') + mtime = os.path.getmtime(filename) data = wrap_file(request.environ, file) rv = current_app.response_class(data, mimetype=mimetype, headers=headers, direct_passthrough=True) + # if we know the file modification date, we can store it as the + # current time to better support conditional requests. Werkzeug + # as of 0.6.1 will override this value however in the conditional + # response with the current time. This will be fixed in Werkzeug + # with a new release, however many WSGI servers will still emit + # a separate date header. + if mtime is not None: + rv.date = int(mtime) + rv.cache_control.public = True if cache_timeout: rv.cache_control.max_age = cache_timeout diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 533afd21..b3b68b37 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -19,7 +19,7 @@ import tempfile from logging import StreamHandler from contextlib import contextmanager from datetime import datetime -from werkzeug import parse_date, parse_options_header +from werkzeug import parse_date, parse_options_header, http_date from werkzeug.exceptions import NotFound from jinja2 import TemplateNotFound from cStringIO import StringIO From f8f8463f3a0e260604b066de56b0a8c7d3a86db4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 14 Jul 2010 02:50:41 +0200 Subject: [PATCH 085/207] Documented cookie problem for #80 --- docs/config.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/config.rst b/docs/config.rst index 0e8a24cd..7d5bd6fc 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -64,6 +64,25 @@ The following configuration values are used internally by Flask: subdomain support (eg: ``'localhost'``) =============================== ========================================= +.. admonition:: More on ``SERVER_NAME`` + + The ``SERVER_NAME`` key is used for the subdomain support. Because + Flask cannot guess the subdomain part without the knowledge of the + actual server name, this is required if you want to work with + subdomains. This is also used for the session cookie. + + Please keep in mind that not only Flask has the problem of not knowing + what subdomains are, your web browser does as well. Most modern web + browsers will not allow cross-subdomain cookies to be set on a + server name without dots in it. So if your server name is + ``'localhost'`` you will not be able to set a cookie for + ``'localhost'`` and every subdomain of it. Please chose a different + server name in that case, like ``'myapplication.local'`` and add + this name + the subdomains you want to use into your host config + or setup a local `bind`_. + +.. _bind: https://www.isc.org/software/bind + .. versionadded:: 0.4 ``LOGGER_NAME`` From b1790cca55956c0cda132ac2fa6a2fb53fd2d009 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 14 Jul 2010 10:47:57 +0200 Subject: [PATCH 086/207] Added MAX_CONTENT_LENGTH config key --- CHANGES | 2 ++ docs/config.rst | 6 ++++++ flask/app.py | 3 ++- flask/wrappers.py | 8 ++++++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 0c04a91d..df24ab9d 100644 --- a/CHANGES +++ b/CHANGES @@ -21,6 +21,8 @@ Release date to be announced, codename to be decided. as `config`. - context processors will no longer override values passed directly to the render function. +- added the ability to limit the incoming request data with the + new ``MAX_CONTENT_LENGTH`` configuration value. Version 0.5.1 ------------- diff --git a/docs/config.rst b/docs/config.rst index 7d5bd6fc..ab1923ba 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -62,6 +62,10 @@ The following configuration values are used internally by Flask: ``LOGGER_NAME`` the name of the logger ``SERVER_NAME`` the name of the server. Required for subdomain support (eg: ``'localhost'``) +``MAX_CONTENT_LENGTH`` If set to a value in bytes, Flask will + reject incoming requests with a + content length greater than this by + returning a 413 status code. =============================== ========================================= .. admonition:: More on ``SERVER_NAME`` @@ -89,6 +93,8 @@ The following configuration values are used internally by Flask: .. versionadded:: 0.5 ``SERVER_NAME`` +.. versionadded:: ``MAX_CONTENT_LENGTH`` + Configuring from Files ---------------------- diff --git a/flask/app.py b/flask/app.py index fd97bab4..415bf753 100644 --- a/flask/app.py +++ b/flask/app.py @@ -193,7 +193,8 @@ class Flask(_PackageBoundObject): 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), 'USE_X_SENDFILE': False, 'LOGGER_NAME': None, - 'SERVER_NAME': None + 'SERVER_NAME': None, + 'MAX_CONTENT_LENGTH': None }) def __init__(self, import_name, static_path=None): diff --git a/flask/wrappers.py b/flask/wrappers.py index b0747564..c578170c 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -13,6 +13,7 @@ from werkzeug import Request as RequestBase, Response as ResponseBase, \ cached_property from .helpers import json, _assert_have_json +from .globals import _request_ctx_stack class Request(RequestBase): @@ -41,6 +42,13 @@ class Request(RequestBase): #: something similar. routing_exception = None + @property + def max_content_length(self): + """Read-only view of the `MAX_CONTENT_LENGTH` config key.""" + ctx = _request_ctx_stack.top + if ctx is not None: + return ctx.app.config['MAX_CONTENT_LENGTH'] + @property def endpoint(self): """The endpoint that matched the request. This in combination with From f5b8c082847baa8a902cef22c43a5d50d7a55bdc Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 14:35:02 +0200 Subject: [PATCH 087/207] endpoint is optional for modules. This fixes #86 --- flask/app.py | 6 ++---- flask/helpers.py | 9 +++++++++ flask/module.py | 15 ++++++++++++--- tests/flask_tests.py | 12 ++++++++++++ 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/flask/app.py b/flask/app.py index 415bf753..16da7939 100644 --- a/flask/app.py +++ b/flask/app.py @@ -23,7 +23,7 @@ from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, NotFound from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ - _tojson_filter + _tojson_filter, _endpoint_from_view_func from .wrappers import Request, Response from .config import ConfigAttribute, Config from .ctx import _RequestContext @@ -496,9 +496,7 @@ class Flask(_PackageBoundObject): added and handled by the standard request handling. """ if endpoint is None: - assert view_func is not None, 'expected view func if endpoint ' \ - 'is not provided.' - endpoint = view_func.__name__ + endpoint = _endpoint_from_view_func(view_func) options['endpoint'] = endpoint methods = options.pop('methods', ('GET',)) provide_automatic_options = False diff --git a/flask/helpers.py b/flask/helpers.py index d76090c4..fc309e67 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -59,6 +59,15 @@ else: _tojson_filter = json.dumps +def _endpoint_from_view_func(view_func): + """Internal helper that returns the default endpoint for a given + function. This always is the function name. + """ + assert view_func is not None, 'expected view func if endpoint ' \ + 'is not provided.' + return view_func.__name__ + + def jsonify(*args, **kwargs): """Creates a :class:`~flask.Response` with the JSON representation of the given arguments with an `application/json` mimetype. The arguments diff --git a/flask/module.py b/flask/module.py index 8935359e..9eaa4f82 100644 --- a/flask/module.py +++ b/flask/module.py @@ -9,7 +9,7 @@ :license: BSD, see LICENSE for more details. """ -from .helpers import _PackageBoundObject +from .helpers import _PackageBoundObject, _endpoint_from_view_func def _register_module(module, static_path): @@ -127,15 +127,24 @@ class Module(_PackageBoundObject): return f return decorator - def add_url_rule(self, rule, endpoint, view_func=None, **options): + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): """Like :meth:`Flask.add_url_rule` but for a module. The endpoint for the :func:`url_for` function is prefixed with the name of the module. + + .. versionchanged:: 0.6 + The `endpoint` argument is now optional and will default to the + function name to consistent with the function of the same name + on the application object. """ def register_rule(state): the_rule = rule if state.url_prefix: the_rule = state.url_prefix + rule - state.app.add_url_rule(the_rule, '%s.%s' % (self.name, endpoint), + the_endpoint = endpoint + if the_endpoint is None: + the_endpoint = _endpoint_from_view_func(view_func) + state.app.add_url_rule(the_rule, '%s.%s' % (self.name, + the_endpoint), view_func, **options) self._record(register_rule) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index b3b68b37..f2451223 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -612,6 +612,18 @@ class ModuleTestCase(unittest.TestCase): assert c.get('/admin/login').data == 'admin login' assert c.get('/admin/logout').data == 'admin logout' + def test_default_endpoint_name(self): + app = flask.Flask(__name__) + mod = flask.Module(__name__, 'frontend') + def index(): + return 'Awesome' + mod.add_url_rule('/', view_func=index) + app.register_module(mod) + rv = app.test_client().get('/') + assert rv.data == 'Awesome' + with app.test_request_context(): + assert flask.url_for('frontend.index') == '/' + def test_request_processing(self): catched = [] app = flask.Flask(__name__) From 4aeb44567ab46951a534fa2836b392ed21b1532b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 14:35:44 +0200 Subject: [PATCH 088/207] Documented change --- CHANGES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index df24ab9d..9e95b349 100644 --- a/CHANGES +++ b/CHANGES @@ -23,6 +23,9 @@ Release date to be announced, codename to be decided. to the render function. - added the ability to limit the incoming request data with the new ``MAX_CONTENT_LENGTH`` configuration value. +- the endpoint for the :meth:`flask.Module.add_url_rule` method + is now optional to be consistent with the function of the + same name on the application object. Version 0.5.1 ------------- From 54ebd88f451cf58b7d07781692fafc465c94305f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 14:39:49 +0200 Subject: [PATCH 089/207] Documented MAX_CONTENT_LENGTH in the file upload docs and linked to Flask-Uploads --- docs/patterns/fileuploads.rst | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst index f4a9a1db..884925cf 100644 --- a/docs/patterns/fileuploads.rst +++ b/docs/patterns/fileuploads.rst @@ -125,27 +125,29 @@ If you now run the application everything should work as expected. Improving Uploads ----------------- +.. versionadded:: 0.6 + So how exactly does Flask handle uploads? Well it will store them in the webserver's memory if the files are reasonable small otherwise in a temporary location (as returned by :func:`tempfile.gettempdir`). But how do you specify the maximum file size after which an upload is aborted? By default Flask will happily accept file uploads to an unlimited amount of -memory, but you can limit that by subclassing the request and overriding -the Werkzeug provided :attr:`~werkzeug.BaseRequest.max_form_memory_size` -attribute:: +memory, but you can limit that by setting the `MAX_CONTENT_LENGTH` +config key:: from flask import Flask, Request - class LimitedRequest(Request): - max_form_memory_size = 16 * 1024 * 1024 - app = Flask(__name__) - app.request_class = LimitedRequest + app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 The code above will limited the maximum allowed payload to 16 megabytes. If a larger file is transmitted, Flask will raise an :exc:`~werkzeug.exceptions.RequestEntityTooLarge` exception. +This feature was added in Flask 0.6 but can be achieved in older versions +as well by subclassing the request object. For more information on that +consult the Werkzeug documentation on file handling. + Upload Progress Bars -------------------- @@ -165,3 +167,14 @@ following libraries for some nice examples how to do that: - `Plupload `_ - HTML5, Java, Flash - `SWFUpload `_ - Flash - `JumpLoader `_ - Java + + +An Easier Solution +------------------ + +Because the common pattern for file uploads exists almost unchanged in all +applications dealing with uploads, there is a Flask extension called +`Flask-Uploads`_ that implements a full fledged upload mechanism with +white and blacklisting of extensions and more. + +.. _Flask-Uploads: http://packages.python.org/Flask-Uploads/ From bc8e021b34a696660410ee60e61664ab969e5cf3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 15:42:27 +0200 Subject: [PATCH 090/207] Added a note on references to static folders --- docs/patterns/packages.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 63d7d76e..f6e1ad87 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -243,3 +243,18 @@ name of the module. So for the admin it would be ``render_template('admin/list_items.html')`` and so on. It is not possible to refer to templates without the prefixed module name. This is explicit unlike URL rules. + +.. admonition:: References to Static Folders + + Please keep in mind that if you are using unqualified endpoints by + default Flask will always assume the module's static folder, even if + there is no such folder. + + If you want to refer to the application's static folder, use a leading + dot:: + + # this refers to the application's static folder + url_for('.static', filename='static.css') + + # this refers to the current module's static folder + url_for('static', filename='static.css') From e84ca8ed94d36e57f7ab8a547096ce7f61c06f5c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 15:43:17 +0200 Subject: [PATCH 091/207] Clearified that --- docs/patterns/packages.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index f6e1ad87..cd2deac1 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -258,3 +258,8 @@ explicit unlike URL rules. # this refers to the current module's static folder url_for('static', filename='static.css') + + This is the case for all endpoints, not just static folders, but for + static folders it's more common that you will stumble upon this because + most applications will have a static folder in the application and not + a specific module. From 793a56bc8b3a42601f0d3e9b6a1f0f73a9c31c33 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 17:01:04 +0200 Subject: [PATCH 092/207] Added the pocoo styleguide to the documentation --- docs/contents.rst.inc | 1 + docs/styleguide.rst | 200 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 docs/styleguide.rst diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index e924202e..27d0b73a 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -44,6 +44,7 @@ Design notes, legal information and changelog are here for the interested. security unicode extensiondev + styleguide upgrading changelog license diff --git a/docs/styleguide.rst b/docs/styleguide.rst new file mode 100644 index 00000000..bb99a79a --- /dev/null +++ b/docs/styleguide.rst @@ -0,0 +1,200 @@ +Pocoo Styleguide +================ + +The Pocoo styleguide is the styleguide for all Pocoo Projects, including +Flask. This styleguide is a requirement for Patches to Flask and a +recommendation for Flask extensions. + +In general the Pocoo Styleguide closely follows :pep:`8` with some small +differences and extensions. + +General Layout +-------------- + +Indentation: + 4 real spaces. No tabs, no exceptions. + +Maximum line length: + 79 characters with a soft limit for 84 if absolutely necessary. Try + to avoid too nested code by cleverly placing `break`, `continue` and + `return` statements. + +Continuing long statements: + To continue a statement you can use backslashes in which case you should + align the next line with the last dot or equal sign, or indent four + spaces:: + + this_is_a_very_long(function_call, 'with many parameters') \ + .that_returns_an_object_with_an_attribute + + MyModel.query.filter(MyModel.scalar > 120) \ + .order_by(MyModel.name.desc()) \ + .limit(10) + + If you break in a statement with parentheses or brances, align to the + braces:: + + this_is_a_very_long(function_call, 'with many parameters', + 23, 42, 'and even more') + + For lists or tuples with many items, break immediately after the + opening brace:: + + items = [ + 'this is the first', 'set of items', 'with more items', + 'to come in this line', 'like this' + ] + +Blank lines: + Top level functions and classes are separated by two lines, everything + else by one. Do not use too many blank lines to separate logical + segments in code. Example:: + + def hello(name): + print 'Hello %s!' % name + + + def goodbye(name): + print 'See you %s.' % name + + + class MyClass(object): + """This is a simple docstring""" + + def __init__(self, name): + self.name = name + + def get_annoying_name(self): + return self.name.upper() + '!!!!111' + +Expressions and Statements +-------------------------- + +General whitespace rules: + - Whitespace is absend for unary operators that are not works + (eg: ``-``, ``~`` etc.) as well on the inner side of parentheses. + - Whitespace is placed between binary operators. + + Good:: + + exp = -1.05 + value = (item_value / item_count) * offset / exp + value = my_list[index] + value = my_dict['key'] + + Bad:: + + exp = - 1.05 + value = ( item_value / item_count ) * offset / exp + value = (item_value/item_count)*offset/exp + value=( item_value/item_count ) * offset/exp + value = my_list[ index ] + value = my_dict ['key'] + +Yoda statements are a nogo: + Never compare constant with variable, always variable with constant: + + Good:: + + if method == 'md5': + pass + + Bad:: + + if 'md5' == method: + pass + +Comparisons: + - against arbitary types: ``==`` and ``!=`` + - against singletones with ``is`` and ``is not`` (eg: ``foo is not + None``) + - never compare something with `True` or `False` (for example never + do ``foo == False``, do ``not foo`` instead) + +Negated containment checks: + use ``foo not in bar`` instead of ``not foo in bar`` + +Instance checks: + ``isinstance(a, C)`` instead of ``type(A) is C``, but try to avoid + instance checks in general. Check for features. + + +Naming Conventions +------------------ + +- Class names: ``CamelCase``, with acronyms kept uppercase (``HTTPWriter`` + and not ``HttpWriter``) +- Variable names: ``lowercase_with_underscores`` +- Method and functin names: ``lowercase_with_underscores`` +- Constants: ``UPPERCASE_WITH_UNDERSCORES`` +- precompiled regular expressions: ``name_re`` + +Protected members are prefixed with a single underscore. Double +underscores are reserved for mixin classes. + +On classes with keywords, trailing underscores are appended. Clashes with +builtins are allowed and **must not** be resolved by appending an +underline to the variable name. If the function needs to access a +shadowed builtin, rebind the builtin to a different name instead. + +Function and method arguments: + - class methods: ``cls`` as first parameter + - instance methods: ``self`` as first parameter + - lambdas for properties might have the first parameter replaced + with ``x`` like in ``display_name = property(lambda x: x.real_name + or x.username)`` + + +Docstrings +---------- + +Docstring conventions: + All docstrings are formatted with reStructuredText as understood by + Sphinx. Depending on the number of lines in the docstring, they are + layed out differently. If it's just one line, the closing tripple + quote is on the same line as the opening, otherwise the text is on + the same line as the opening quote and the tripple quote that closes + the string on its own line:: + + def foo(): + """This is a simple docstring""" + + + def bar(): + """This is a longer docstring with so much information in there + that it spans three lines. In this case the closing tripple quote + is on its own line. + """ + +Module header: + The module header consists of an utf-8 encoding declaration (if non + ASCII letters are used, but it is recommended all the time) and a + standard docstring:: + + # -*- coding: utf-8 -*- + """ + package.module + ~~~~~~~~~~~~~~ + + A brief description goes here. + + :copyright: (c) YEAR by AUTHOR. + :license: LICENSE_NAME, see LICENSE_FILE for more details. + """ + + Please keep in mind that proper copyrights and license files are a + requirement for approved Flask extensions. + + +Comments +-------- + +Rules for comments are similar to docstrings. Both are formatted with +reStructuredText. If a comment is used to document an attribute, put a +colon after the opening pound sign (``#``):: + + class User(object): + #: the name of the user as unicode string + name = Column(String) + #: the sha1 hash of the password + inline salt + pw_hash = Column(String) From 1202996c634b23f82421753cb7c46a5ccdf7aca2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 18:10:26 +0200 Subject: [PATCH 093/207] Fixed a template loading bug --- flask/templating.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/flask/templating.py b/flask/templating.py index c55d4826..a6fd0982 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -35,23 +35,20 @@ class _DispatchingJinjaLoader(BaseLoader): self.app = app def get_source(self, environment, template): + loader = None try: module, name = template.split('/', 1) loader = self.app.modules[module].jinja_loader - if loader is None: - raise ValueError() except (ValueError, KeyError): - loader = self.app.jinja_loader - if loader is not None: - return loader.get_source(environment, template) - else: + pass + # if there was a module and it has a loader, try this first + if loader is not None: try: return loader.get_source(environment, name) except TemplateNotFound: pass - # raise the exception with the correct filename here. - # (the one that includes the prefix) - raise TemplateNotFound(template) + # fall back to application loader if module failed + return self.app.jinja_loader.get_source(environment, template) def list_templates(self): result = self.app.jinja_loader.list_templates() From c83b5555f05ebcff450908684bbf793ca561bea2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 18:11:38 +0200 Subject: [PATCH 094/207] Improved styleguide docs --- docs/styleguide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/styleguide.rst b/docs/styleguide.rst index bb99a79a..1387d4a6 100644 --- a/docs/styleguide.rst +++ b/docs/styleguide.rst @@ -71,7 +71,7 @@ Expressions and Statements -------------------------- General whitespace rules: - - Whitespace is absend for unary operators that are not works + - No whitespace for unary operators that are not words (eg: ``-``, ``~`` etc.) as well on the inner side of parentheses. - Whitespace is placed between binary operators. From 82b143f9720ea5837289af32785ce5ef951943d9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 20:03:58 +0200 Subject: [PATCH 095/207] Changelog entry for 0.5.2 Conflicts: CHANGES --- CHANGES | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES b/CHANGES index 9e95b349..3b77329f 100644 --- a/CHANGES +++ b/CHANGES @@ -27,6 +27,14 @@ Release date to be announced, codename to be decided. is now optional to be consistent with the function of the same name on the application object. +Version 0.5.2 +------------- + +Bugfix Release, released on July 15th 2010 + +- fixed another issue with loading templates from directories when + modules were used. + Version 0.5.1 ------------- From dbb3620792eda43d5f778c7f9019609f62ad2bb9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 21:22:11 +0200 Subject: [PATCH 096/207] Fixed an rst error --- docs/config.rst | 3 ++- docs/patterns/fileuploads.rst | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index ab1923ba..e782bc7f 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -93,7 +93,8 @@ The following configuration values are used internally by Flask: .. versionadded:: 0.5 ``SERVER_NAME`` -.. versionadded:: ``MAX_CONTENT_LENGTH`` +.. versionadded:: 0.6 + ``MAX_CONTENT_LENGTH`` Configuring from Files ---------------------- diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst index 884925cf..99f009c7 100644 --- a/docs/patterns/fileuploads.rst +++ b/docs/patterns/fileuploads.rst @@ -132,7 +132,7 @@ webserver's memory if the files are reasonable small otherwise in a temporary location (as returned by :func:`tempfile.gettempdir`). But how do you specify the maximum file size after which an upload is aborted? By default Flask will happily accept file uploads to an unlimited amount of -memory, but you can limit that by setting the `MAX_CONTENT_LENGTH` +memory, but you can limit that by setting the ``MAX_CONTENT_LENGTH`` config key:: from flask import Flask, Request From b4b2f42f48684fbb4c2d56f4f83d01b2be421a39 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Fri, 16 Jul 2010 01:18:20 +0800 Subject: [PATCH 097/207] Warn about SQL injection in the tutorial. --- docs/tutorial/views.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst index 0bce03a3..f2871257 100644 --- a/docs/tutorial/views.rst +++ b/docs/tutorial/views.rst @@ -48,6 +48,13 @@ redirect back to the `show_entries` page:: Note that we check that the user is logged in here (the `logged_in` key is present in the session and `True`). +.. admonition:: Security Note + + Be sure to use question marks when building SQL statements, as done in the + example above. Otherwise, your app will be vulnerable to SQL injection when + you use string formatting to build SQL statements. + See :ref:`sqlite3` for more. + Login and Logout ---------------- From 70dc2b66a0d02a23546ad8d48b7fca1c21997ae6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Jul 2010 13:14:54 +0200 Subject: [PATCH 098/207] Removed useless script reference. This fixes #87 --- docs/errorhandling.rst | 8 ++++---- examples/jqueryexample/templates/layout.html | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index ac9595f1..82d53b7c 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -71,9 +71,9 @@ We also tell the handler to only send errors and more critical messages. Because we certainly don't want to get a mail for warnings or other useless logs that might happen during request handling. -Before you run that in production, please also look at :ref:`log-format` -to put more information into that error mail. That will save you from a -lot of frustration. +Before you run that in production, please also look at :ref:`logformat` to +put more information into that error mail. That will save you from a lot +of frustration. Logging to a File @@ -110,7 +110,7 @@ above, just make sure to use a lower setting (I would recommend file_handler.setLevel(logging.WARNING) app.logger.addHandler(file_handler) -.. _log-format: +.. _logformat: Controlling the Log Format -------------------------- diff --git a/examples/jqueryexample/templates/layout.html b/examples/jqueryexample/templates/layout.html index 0b5f3a7e..3e2ed69b 100644 --- a/examples/jqueryexample/templates/layout.html +++ b/examples/jqueryexample/templates/layout.html @@ -2,8 +2,6 @@ jQuery Example - From 6fc1492357a94ab553fe7ef16a7a2235d05f5a1d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 11:36:16 +0200 Subject: [PATCH 099/207] Added make_response --- CHANGES | 2 ++ docs/api.rst | 2 ++ flask/__init__.py | 2 +- flask/helpers.py | 42 ++++++++++++++++++++++++++++++++++++++++++ tests/flask_tests.py | 18 ++++++++++++++++++ 5 files changed, 65 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 3b77329f..24a6162e 100644 --- a/CHANGES +++ b/CHANGES @@ -26,6 +26,8 @@ Release date to be announced, codename to be decided. - the endpoint for the :meth:`flask.Module.add_url_rule` method is now optional to be consistent with the function of the same name on the application object. +- added a :func:`flask.make_response` function that simplifies + creating response object instances in views. Version 0.5.2 ------------- diff --git a/docs/api.rst b/docs/api.rst index d7f887e4..f31563b4 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -228,6 +228,8 @@ Useful Functions and Classes .. autofunction:: redirect +.. autofunction:: make_response + .. autofunction:: send_file .. autofunction:: send_from_directory diff --git a/flask/__init__.py b/flask/__init__.py index f4617271..93ada5f7 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -19,7 +19,7 @@ from .app import Flask, Request, Response from .config import Config from .helpers import url_for, jsonify, json_available, flash, \ send_file, send_from_directory, get_flashed_messages, \ - get_template_attribute + get_template_attribute, make_response from .globals import current_app, g, request, session, _request_ctx_stack from .module import Module from .templating import render_template, render_template_string diff --git a/flask/helpers.py b/flask/helpers.py index fc309e67..6b57420c 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -101,6 +101,48 @@ def jsonify(*args, **kwargs): indent=None if request.is_xhr else 2), mimetype='application/json') +def make_response(*args): + """Sometimes it is necessary to set additional headers in a view. Because + views do not have to return response objects but can return a value that + is converted into a response object by Flask itself, it becomes tricky to + add headers to it. This function can be called instead of using a return + and you will get a response object which you can use to attach headers. + + If view looked like this and you want to add a new header:: + + def index(): + return render_template('index.html', foo=42) + + You can now do something like this:: + + def index(): + response = make_response(render_template('index.html', foo=42)) + response.headers['X-Parachutes'] = 'parachutes are cool' + return response + + This function accepts the very same arguments you can return from a + view function. This for example creates a response with a 404 error + code:: + + response = make_response(render_template('not_found.html', 404)) + + Internally this function does the following things: + + - if no arguments are passed, it creates a new response argument + - if one argument is passed, :meth:`flask.Flask.make_response` + is invoked with it. + - if more than one argument is passed, the arguments are passed + to the :meth:`flask.Flask.make_response` function as tuple. + + .. versionadded:: 0.6 + """ + if not args: + return current_app.response_class() + if len(args) == 1: + args = args[0] + return current_app.make_response(args) + + def url_for(endpoint, **values): """Generates a URL to the given endpoint with the method provided. The endpoint is relative to the active module if modules are in use. diff --git a/tests/flask_tests.py b/tests/flask_tests.py index f2451223..ccf9b871 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -393,6 +393,24 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert rv.status_code == 400 assert rv.mimetype == 'text/plain' + def test_make_response(self): + app = flask.Flask(__name__) + with app.test_request_context(): + rv = flask.make_response() + assert rv.status_code == 200 + assert rv.data == '' + assert rv.mimetype == 'text/html' + + rv = flask.make_response('Awesome') + assert rv.status_code == 200 + assert rv.data == 'Awesome' + assert rv.mimetype == 'text/html' + + rv = flask.make_response('W00t', 404) + assert rv.status_code == 404 + assert rv.data == 'W00t' + assert rv.mimetype == 'text/html' + def test_url_generation(self): app = flask.Flask(__name__) @app.route('/hello/', methods=['POST']) From 90b8df3e4ce762ac4de4a6fabd8fb34532bedd5e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 13:10:50 +0200 Subject: [PATCH 100/207] Fixed paren in docstring --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 6b57420c..39214a10 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -124,7 +124,7 @@ def make_response(*args): view function. This for example creates a response with a 404 error code:: - response = make_response(render_template('not_found.html', 404)) + response = make_response(render_template('not_found.html'), 404) Internally this function does the following things: From a59dfe4a77ecfd8740cf55a719c88eedee07fb5c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 14:02:02 +0200 Subject: [PATCH 101/207] Added missing template --- tests/templates/nested/nested.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/templates/nested/nested.txt diff --git a/tests/templates/nested/nested.txt b/tests/templates/nested/nested.txt new file mode 100644 index 00000000..2c8634f9 --- /dev/null +++ b/tests/templates/nested/nested.txt @@ -0,0 +1 @@ +I'm nested From e0712b47c6e5bc98f2afd8b0a82cc5005dc58722 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 14:39:28 +0200 Subject: [PATCH 102/207] Added support for signals --- CHANGES | 5 ++ Makefile | 5 +- docs/api.rst | 51 ++++++++++++++++++ docs/conf.py | 3 +- flask/__init__.py | 4 ++ flask/app.py | 4 ++ flask/signals.py | 50 ++++++++++++++++++ flask/templating.py | 14 ++++- tests/flask_tests.py | 79 ++++++++++++++++++++++++++++ tests/templates/simple_template.html | 1 + 10 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 flask/signals.py create mode 100644 tests/templates/simple_template.html diff --git a/CHANGES b/CHANGES index 24a6162e..30ba418b 100644 --- a/CHANGES +++ b/CHANGES @@ -28,6 +28,11 @@ Release date to be announced, codename to be decided. same name on the application object. - added a :func:`flask.make_response` function that simplifies creating response object instances in views. +- added signalling support based on blinker. This feature is currently + optional and supposed to be used by extensions and applications. If + you want to use it, make sure to have `blinker`_ installed. + +.. _blinker: http://pypi.python.org/pypi/blinker Version 0.5.2 ------------- diff --git a/Makefile b/Makefile index 4b1d8081..ba898b9e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean-pyc test upload-docs +.PHONY: clean-pyc test upload-docs docs all: clean-pyc test @@ -20,3 +20,6 @@ upload-docs: scp -r docs/_build/dirhtml/* pocoo.org:/var/www/flask.pocoo.org/docs/ scp -r docs/_build/latex/Flask.pdf pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.pdf scp -r docs/_build/flask-docs.zip pocoo.org:/var/www/flask.pocoo.org/docs/ + +docs: + $(MAKE) -C docs html diff --git a/docs/api.rst b/docs/api.rst index f31563b4..afcb4c20 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -351,3 +351,54 @@ Useful Internals information from the context local around for a little longer. Make sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in that situation, otherwise your unittests will leak memory. + +Signals +------- + +.. versionadded:: 0.6 + +.. data:: signals_available + + `True` if the signalling system is available. This is the case + when `blinker`_ is installed. + +.. data:: template_rendered + + This signal is sent when a template was successfully rendered. The + signal is invoked with the instance of the template as `template` + and the context as dictionary (named `context`). + +.. data:: request_started + + This signal is sent before any request processing started but when the + request context was set up. Because the request context is already + bound, the subscriber can access the request with the standard global + proxies such as :class:`~flask.request`. + +.. data:: request_finished + + This signal is sent right before the response is sent to the client. + It is passed the response to be sent named `response`. + +.. data:: got_request_exception + + This signal is sent when an exception happens during request processing. + It is sent *before* the standard exception handling kicks in and even + in debug mode, where no exception handling happens. The exception + itself is passed to the subscriber as `exception`. + +.. class:: flask.signals.Namespace + + An alias for :class:`blinker.base.Namespace` if blinker is available, + otherwise a dummy class that creates fake signals. This class is + available for Flask extensions that want to provide the same fallback + system as Flask itself. + + .. method:: signal(name, doc=None) + + Creates a new signal for this namespace if blinker is available, + otherwise returns a fake signal that has a send method that will + do nothing but will fail with a :exc:`RuntimeError` for all other + operations, including connecting. + +.. _blinker: http://pypi.python.org/pypi/blinker diff --git a/docs/conf.py b/docs/conf.py index 6308beee..721c8369 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -245,7 +245,8 @@ intersphinx_mapping = { 'http://docs.python.org/dev': None, 'http://werkzeug.pocoo.org/documentation/dev/': None, 'http://www.sqlalchemy.org/docs/': None, - 'http://wtforms.simplecodes.com/docs/0.5/': None + 'http://wtforms.simplecodes.com/docs/0.5/': None, + 'http://discorporate.us/projects/Blinker/docs/1.0/': None } pygments_style = 'flask_theme_support.FlaskyStyle' diff --git a/flask/__init__.py b/flask/__init__.py index 93ada5f7..95497069 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -24,6 +24,10 @@ from .globals import current_app, g, request, session, _request_ctx_stack from .module import Module from .templating import render_template, render_template_string +# the signals +from .signals import signals_available, template_rendered, request_started, \ + request_finished, got_request_exception + # only import json if it's available if json_available: from .helpers import json diff --git a/flask/app.py b/flask/app.py index 16da7939..5860eb05 100644 --- a/flask/app.py +++ b/flask/app.py @@ -32,6 +32,7 @@ from .session import Session, _NullSession from .module import _ModuleSetupState from .templating import _DispatchingJinjaLoader, \ _default_template_ctx_processor +from .signals import request_started, request_finished, got_request_exception # a lock used for logger initialization _logger_lock = Lock() @@ -657,6 +658,7 @@ class Flask(_PackageBoundObject): .. versionadded: 0.3 """ + got_request_exception.send(self, exception=e) handler = self.error_handlers.get(500) if self.debug: raise @@ -791,6 +793,7 @@ class Flask(_PackageBoundObject): """ with self.request_context(environ): try: + request_started.send(self) rv = self.preprocess_request() if rv is None: rv = self.dispatch_request() @@ -801,6 +804,7 @@ class Flask(_PackageBoundObject): response = self.process_response(response) except Exception, e: response = self.make_response(self.handle_exception(e)) + request_finished.send(self, response=response) return response(environ, start_response) def request_context(self, environ): diff --git a/flask/signals.py b/flask/signals.py new file mode 100644 index 00000000..a7e459af --- /dev/null +++ b/flask/signals.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +""" + flask.signals + ~~~~~~~~~~~~~ + + Implements signals based on blinker if available, otherwise + falls silently back to a noop + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +signals_available = False +try: + from blinker import Namespace + signals_available = True + _signals = Namespace() +except ImportError: + class Namespace(object): + def signal(self, name, doc=None): + return _FakeSignal(name, doc) + class _FakeSignal(object): + """If blinker is unavailable, create a fake class with the same + interface that allows sending of signals but will fail with an + error on anything else. Instead of doing anything on send, it + will just ignore the arguments and do nothing instead. + """ + + def __init__(self, name, doc=None): + self.name = name + self.__doc__ = doc + def _fail(self, *args, **kwargs): + raise RuntimeError('signalling support is unavailable ' + 'because the blinker library is ' + 'not installed.') + send = lambda *a, **kw: None + connect = disconnect = has_receivers_for = receivers_for = \ + temporarily_connected_to = _fail + del _fail + +# the namespace for code signals. If you are not flask code, do +# not put signals in here. Create your own namespace instead. +_signals = Namespace() + + +# core signals. For usage examples grep the sourcecode or consult +# the API documentation in docs/api.rst as well as docs/signals.rst +template_rendered = _signals.signal('template-rendered') +request_started = _signals.signal('request-started') +request_finished = _signals.signal('request-finished') +got_request_exception = _signals.signal('got-request-exception') diff --git a/flask/templating.py b/flask/templating.py index a6fd0982..41c0d39b 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -11,6 +11,7 @@ from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound from .globals import _request_ctx_stack +from .signals import template_rendered def _default_template_ctx_processor(): @@ -59,6 +60,13 @@ class _DispatchingJinjaLoader(BaseLoader): return result +def _render(template, context, app): + """Renders the template and fires the signal""" + rv = template.render(context) + template_rendered.send(app, template=template, context=context) + return rv + + def render_template(template_name, **context): """Renders a template from the template folder with the given context. @@ -69,7 +77,8 @@ def render_template(template_name, **context): """ ctx = _request_ctx_stack.top ctx.app.update_template_context(context) - return ctx.app.jinja_env.get_template(template_name).render(context) + return _render(ctx.app.jinja_env.get_template(template_name), + context, ctx.app) def render_template_string(source, **context): @@ -83,4 +92,5 @@ def render_template_string(source, **context): """ ctx = _request_ctx_stack.top ctx.app.update_template_context(context) - return ctx.app.jinja_env.from_string(source).render(context) + return _render(ctx.app.jinja_env.from_string(source), + context, ctx.app) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index ccf9b871..43665e1c 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1024,6 +1024,83 @@ class SubdomainTestCase(unittest.TestCase): assert rv.data == 'index for mitsuhiko' +class TestSignals(unittest.TestCase): + + def test_template_rendered(self): + app = flask.Flask(__name__) + + @app.route('/') + def index(): + return flask.render_template('simple_template.html', whiskey=42) + + recorded = [] + def record(sender, template, context): + recorded.append((template, context)) + + with flask.template_rendered.temporarily_connected_to(record, app): + rv = app.test_client().get('/') + assert len(recorded) == 1 + template, context = recorded[0] + assert template.name == 'simple_template.html' + assert context['whiskey'] == 42 + + def test_request_signals(self): + app = flask.Flask(__name__) + calls = [] + + def before_request_signal(sender): + calls.append('before-signal') + + def after_request_signal(sender, response): + assert response.data == 'stuff' + calls.append('after-signal') + + @app.before_request + def before_request_handler(): + calls.append('before-handler') + + @app.after_request + def after_request_handler(response): + calls.append('after-handler') + response.data = 'stuff' + return response + + @app.route('/') + def index(): + calls.append('handler') + return 'ignored anyway' + + flask.request_started.connect(before_request_signal, app) + flask.request_finished.connect(after_request_signal, app) + + try: + rv = app.test_client().get('/') + assert rv.data == 'stuff' + + assert calls == ['before-signal', 'before-handler', + 'handler', 'after-handler', + 'after-signal'] + finally: + flask.request_started.disconnect(before_request_signal, app) + flask.request_finished.disconnect(after_request_signal, app) + + def test_request_exception_signal(self): + app = flask.Flask(__name__) + recorded = [] + + @app.route('/') + def index(): + 1/0 + + def record(sender, exception): + recorded.append(exception) + + with flask.got_request_exception.temporarily_connected_to(record): + assert app.test_client().get('/').status_code == 500 + assert len(recorded) == 1 + assert isinstance(recorded[0], ZeroDivisionError) + + def suite(): from minitwit_tests import MiniTwitTestCase from flaskr_tests import FlaskrTestCase @@ -1038,6 +1115,8 @@ def suite(): suite.addTest(unittest.makeSuite(SubdomainTestCase)) if flask.json_available: suite.addTest(unittest.makeSuite(JSONTestCase)) + if flask.signals_available: + suite.addTest(unittest.makeSuite(TestSignals)) suite.addTest(unittest.makeSuite(MiniTwitTestCase)) suite.addTest(unittest.makeSuite(FlaskrTestCase)) return suite diff --git a/tests/templates/simple_template.html b/tests/templates/simple_template.html new file mode 100644 index 00000000..c24612cb --- /dev/null +++ b/tests/templates/simple_template.html @@ -0,0 +1 @@ +

{{ whiskey }}

From c360f005c354f406735f4119d057a75b24e7baf1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 15:25:53 +0200 Subject: [PATCH 103/207] Added signal documentation --- docs/api.rst | 2 + docs/contents.rst.inc | 1 + docs/signals.rst | 205 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 docs/signals.rst diff --git a/docs/api.rst b/docs/api.rst index afcb4c20..ca57e8bf 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -355,6 +355,8 @@ Useful Internals Signals ------- +.. when modifying this list, also update the one in signals.rst + .. versionadded:: 0.6 .. data:: signals_available diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index 27d0b73a..ba2de78f 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -15,6 +15,7 @@ instructions for web development with Flask. testing errorhandling config + signals shell patterns/index deploying/index diff --git a/docs/signals.rst b/docs/signals.rst new file mode 100644 index 00000000..7f2d4d97 --- /dev/null +++ b/docs/signals.rst @@ -0,0 +1,205 @@ +.. _signals: + +Signals +======= + +.. versionadded:: 0.6 + +Starting with Flask 0.6, there is integrated support for signalling in +Flask. This support is provided by the excellent `blinker`_ library and +will gracefully fall back if it is not available. + +What are signals? Signals help you decouple applications by sending +notifications when actions occur elsewhere in the core framework or +another Flask extensions. In short, signals allow certain senders to +notify subscribers that something happened. + +Flask comes with a couple of signals and other extensions might provide +more. Also keep in mind that signals are intended to notify subscribers +and should not encourage subscribers to modify data. You will notice that +there are signals that appear to do the same thing like some of the +builtin decorators do (eg: :data:`~flask.request_started` is very similar +to :meth:`~flask.Flask.before_request`). There are however difference in +how they work. The core :meth:`~flask.Flask.before_request` handler for +example is executed in a specific order and is able to abort the request +early by returning a response. In contrast all signal handlers are +executed in undefined order and do not modify any data. + +The big advantage of signals over handlers is that you can safely +subscribe to them for the split of a second. These temporary +subscriptions are helpful for unittesting for example. Say you want to +know what templates were rendered as part of a request: signals allow you +to do exactly that. + +Subscribing to Signals +---------------------- + +To subscribe to a signal, you can use the +:meth:`~blinker.base.Signal.connect` method of a signal. The first +argument is the function that should be called when the signal is emitted, +the optional second argument specifies a sender. To unsubscribe from a +signal, you can use the :meth:`~blinker.base.Signal.disconnect` method. + +For all core Flask signals, the sender is the application that issued the +signal. This however might not be true for Flask extensions, so consult +the documentation when subscribing to signals. + +Additionally there is a convenient helper method that allows you to +temporarily subscribe a function to a signal. This is especially helpful +for unittests (:meth:`~blinker.base.Signal.temporarily_connected_to`). +This has to be used in combination with the `with` statement. + +Here for example a helper context manager that can be used to figure out +in a unittest which templates were rendered and what variables were passed +to the template:: + + from flask import template_rendered + from contextlib import contextmanager + + @contextmanager + def captured_templates(): + recorded = [] + def record(template, context): + recorded.append((template, context)) + template_rendered.connect(record) + try: + yield templates + finally: + template_rendered.disconnect(record) + +This can now easily be paired with a test client:: + + with captured_templates() as templates: + rv = app.test_client().get('/') + assert rv.status_code == 200 + assert len(templates) == 1 + template, context = templates[0] + assert template.name == 'index.html' + assert len(context['items']) == 10 + +All the template rendering in the code, the `with` block wraps will now be +recorded in the `templates` variable. Whenever a template is rendered, +the template object as well as context is appended to it. + +Creating Signals +---------------- + +If you want to use signals in your own application, you can use the +blinker library directly. The most common use case are named signals in a +custom :class:`~blinker.base.Namespace`.. This is what is recommended +most of the time:: + + from blinker import Namespace + my_signals = Namespace() + +Now you can create new signals like this:: + + model_saved = my_signals.signal('model-saved') + +The name for the signal here makes it unique and also simplifies +debugging. You can access the name of the signal with the +:attr:`~blinker.base.NamedSignal.name` attribute. + +.. admonition:: For Extension Developers + + If you are writing a Flask extension and you to gracefully degrade for + missing blinker installations, you can do so by using the + :class:`flask.signals.Namespace` class. + +Sending Signals +--------------- + +If you want to emit a signal, you can do so by calling the +:meth:`~blinker.base.Signal.send` method. It accepts a sender as first +argument and optionally some keyword arguments that are forwarded to the +signal subscribers:: + + class Model(object): + ... + + def save(self): + model_saved.send(self) + +Try to always pick a good sender. If you have a class that is emitting a +signal, pass `self` as sender. If you emitting a signal from a random +function, you can pass ``current_app._get_current_object()`` as sender. + +.. admonition:: Passing Proxies as Senders + + Never pass :data:`~flask.current_app` as sender to a signal. Use + ``current_app._get_current_object()`` instead. The reason for this is + that :data:`~flask.current_app` is a proxy and not the real application + object. + +Core Signals +------------ + +.. when modifying this list, also update the one in api.rst + +The following signals exist in Flask: + +.. data:: flask.template_rendered + :noindex: + + This signal is sent when a template was successfully rendered. The + signal is invoked with the instance of the template as `template` + and the context as dictionary (named `context`). + + Example subscriber:: + + def log_template_renders(sender, template, context): + sender.logger.debug('Rendering template "%s" with context %s', + template.name or 'string template', + context) + + from flask import request_started + request_started.connect(log_template_renders) + +.. data:: flask.request_started + :noindex: + + This signal is sent before any request processing started but when the + request context was set up. Because the request context is already + bound, the subscriber can access the request with the standard global + proxies such as :class:`~flask.request`. + + Example subscriber:: + + def log_request(sender): + sender.logger.debug('Request context is set up') + + from flask import request_started + request_started.connect(log_request) + +.. data:: flask.request_finished + :noindex: + + This signal is sent right before the response is sent to the client. + It is passed the response to be sent named `response`. + + Example subscriber:: + + def log_response(sender, response): + sender.logger.debug('Request context is about to close down. ' + 'Response: %s', response) + + from flask import request_finished + request_finished.connect(log_response) + +.. data:: flask.got_request_exception + :noindex: + + This signal is sent when an exception happens during request processing. + It is sent *before* the standard exception handling kicks in and even + in debug mode, where no exception handling happens. The exception + itself is passed to the subscriber as `exception`. + + Example subscriber:: + + def log_exception(sender, exception): + sender.logger.debug('Got exception during processing: %s', exception) + + from flask import got_request_exception + got_request_exception.connect(log_exception) + +.. _blinker: http://pypi.python.org/pypi/blinker From 91e9632a379ebdd778c229a6da86d11140fa7f17 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 15:26:35 +0200 Subject: [PATCH 104/207] Moved wsgi_app down to a more logical location --- flask/app.py | 82 ++++++++++++++++++++++++++-------------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/flask/app.py b/flask/app.py index 5860eb05..351c0c3a 100644 --- a/flask/app.py +++ b/flask/app.py @@ -766,47 +766,6 @@ class Flask(_PackageBoundObject): response = handler(response) return response - def wsgi_app(self, environ, start_response): - """The actual WSGI application. This is not implemented in - `__call__` so that middlewares can be applied without losing a - reference to the class. So instead of doing this:: - - app = MyMiddleware(app) - - It's a better idea to do this instead:: - - app.wsgi_app = MyMiddleware(app.wsgi_app) - - Then you still have the original application object around and - can continue to call methods on it. - - .. versionchanged:: 0.4 - The :meth:`after_request` functions are now called even if an - error handler took over request processing. This ensures that - even if an exception happens database have the chance to - properly close the connection. - - :param environ: a WSGI environment - :param start_response: a callable accepting a status code, - a list of headers and an optional - exception context to start the response - """ - with self.request_context(environ): - try: - request_started.send(self) - rv = self.preprocess_request() - if rv is None: - rv = self.dispatch_request() - response = self.make_response(rv) - except Exception, e: - response = self.make_response(self.handle_exception(e)) - try: - response = self.process_response(response) - except Exception, e: - response = self.make_response(self.handle_exception(e)) - request_finished.send(self, response=response) - return response(environ, start_response) - def request_context(self, environ): """Creates a request context from the given environment and binds it to the current context. This must be used in combination with @@ -854,6 +813,47 @@ class Flask(_PackageBoundObject): from werkzeug import create_environ return self.request_context(create_environ(*args, **kwargs)) + def wsgi_app(self, environ, start_response): + """The actual WSGI application. This is not implemented in + `__call__` so that middlewares can be applied without losing a + reference to the class. So instead of doing this:: + + app = MyMiddleware(app) + + It's a better idea to do this instead:: + + app.wsgi_app = MyMiddleware(app.wsgi_app) + + Then you still have the original application object around and + can continue to call methods on it. + + .. versionchanged:: 0.4 + The :meth:`after_request` functions are now called even if an + error handler took over request processing. This ensures that + even if an exception happens database have the chance to + properly close the connection. + + :param environ: a WSGI environment + :param start_response: a callable accepting a status code, + a list of headers and an optional + exception context to start the response + """ + with self.request_context(environ): + try: + request_started.send(self) + rv = self.preprocess_request() + if rv is None: + rv = self.dispatch_request() + response = self.make_response(rv) + except Exception, e: + response = self.make_response(self.handle_exception(e)) + try: + response = self.process_response(response) + except Exception, e: + response = self.make_response(self.handle_exception(e)) + request_finished.send(self, response=response) + return response(environ, start_response) + def __call__(self, environ, start_response): """Shortcut for :attr:`wsgi_app`.""" return self.wsgi_app(environ, start_response) From a3a72e2d8d8df53164689fd84a5db2c6cd04f28a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 15:39:24 +0200 Subject: [PATCH 105/207] Added middlewares to quickstart. This fixes #88 --- docs/quickstart.rst | 11 +++++++++++ flask/app.py | 10 ++++++++++ flask/ctx.py | 3 +-- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 4e98f858..3604bdd1 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -734,3 +734,14 @@ Here are some example log calls:: The attached :attr:`~flask.Flask.logger` is a standard logging :class:`~logging.Logger`, so head over to the official stdlib documentation for more information. + +Hooking in WSGI Middlewares +--------------------------- + +If you want to add a WSGI middleware to your application you can wrap the +internal WSGI application. For example if you want to one of the +middlewares from the Werkzeug package to work around bugs in lighttpd, you +can do it like this:: + + from werkzeug.contrib.fixers import LighttpdCGIRootFix + app.wsgi_app = LighttpdCGIRootFix(app.wsgi_app) diff --git a/flask/app.py b/flask/app.py index 351c0c3a..21235897 100644 --- a/flask/app.py +++ b/flask/app.py @@ -724,6 +724,16 @@ class Flask(_PackageBoundObject): return self.response_class(*rv) return self.response_class.force_type(rv, request.environ) + def create_url_adapter(self, request): + """Creates a URL adapter for the given request. The URL adapter + is created at a point where the request context is not yet set up + so the request is passed explicitly. + + .. versionadded:: 0.6 + """ + return self.url_map.bind_to_environ(request.environ, + server_name=self.config['SERVER_NAME']) + def preprocess_request(self): """Called before the actual request dispatching and will call every as :meth:`before_request` decorated function. diff --git a/flask/ctx.py b/flask/ctx.py index 854503af..1b17086c 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -28,9 +28,8 @@ class _RequestContext(object): def __init__(self, app, environ): self.app = app - self.url_adapter = app.url_map.bind_to_environ(environ, - server_name=app.config['SERVER_NAME']) self.request = app.request_class(environ) + self.url_adapter = app.create_url_adapter(self.request) self.session = app.open_session(self.request) if self.session is None: self.session = _NullSession() From e5008386b1a7e0ef3ebb5a4082bf2176d7169ab9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 15:41:05 +0200 Subject: [PATCH 106/207] Added docs on use_evalex. This fixes #90 --- flask/app.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flask/app.py b/flask/app.py index 21235897..0941cb7e 100644 --- a/flask/app.py +++ b/flask/app.py @@ -371,6 +371,11 @@ class Flask(_PackageBoundObject): :attr:`debug` flag is set the server will automatically reload for code changes and show a debugger in case an exception happened. + If you want to run the application in debug mode, but disable the + code execution on the interactive debugger, you can pass + ``use_evalex=False`` as parameter. This will keep the debugger's + traceback screen active, but disable code execution. + .. admonition:: Keep in Mind Flask will suppress any server error with a generic error page From b6cae028f7a2c77108e5ef91e34c285a9ae7ad81 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 15:53:32 +0200 Subject: [PATCH 107/207] Removed useless instanciation --- flask/signals.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flask/signals.py b/flask/signals.py index a7e459af..17de0447 100644 --- a/flask/signals.py +++ b/flask/signals.py @@ -13,7 +13,6 @@ signals_available = False try: from blinker import Namespace signals_available = True - _signals = Namespace() except ImportError: class Namespace(object): def signal(self, name, doc=None): From 405d4492e41b50f204d7e7d55aa0ac8f11edda1a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 15:56:58 +0200 Subject: [PATCH 108/207] Documented more changes --- CHANGES | 3 +++ flask/app.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 30ba418b..842c1a9c 100644 --- a/CHANGES +++ b/CHANGES @@ -31,6 +31,9 @@ Release date to be announced, codename to be decided. - added signalling support based on blinker. This feature is currently optional and supposed to be used by extensions and applications. If you want to use it, make sure to have `blinker`_ installed. +- refactored the way url adapters are created. This process is now + fully customizable with the :meth:`~flask.Flask.create_url_adapter` + method. .. _blinker: http://pypi.python.org/pypi/blinker diff --git a/flask/app.py b/flask/app.py index 0941cb7e..c5f50135 100644 --- a/flask/app.py +++ b/flask/app.py @@ -311,7 +311,7 @@ class Flask(_PackageBoundObject): def create_jinja_environment(self): """Creates the Jinja2 environment based on :attr:`jinja_options` - and :meth:`create_jinja_loader`. + and :meth:`select_jinja_autoescape`. .. versionadded:: 0.5 """ From 3ebc97a7809a15ae5f362d1330552db1efdec525 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 16:02:21 +0200 Subject: [PATCH 109/207] Fixed a typo in quickstart --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 3604bdd1..dc292e8f 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -739,7 +739,7 @@ Hooking in WSGI Middlewares --------------------------- If you want to add a WSGI middleware to your application you can wrap the -internal WSGI application. For example if you want to one of the +internal WSGI application. For example if you want to use one of the middlewares from the Werkzeug package to work around bugs in lighttpd, you can do it like this:: From 97d2a198e216b29be5533dfba81525481bc9c1e5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 16:05:06 +0200 Subject: [PATCH 110/207] Added fallback for docs --- docs/conf.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 721c8369..932c9bcd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -250,3 +250,17 @@ intersphinx_mapping = { } pygments_style = 'flask_theme_support.FlaskyStyle' + +# fall back if theme is not there +try: + __import__('flask_theme_support') +except ImportError, e: + print '-' * 74 + print 'Warning: Flask themes unavailable. Building with default theme' + print 'If you want the Flask themes, run this command and build again:' + print + print ' git submodule init' + print '-' * 74 + + pygments_style = 'tango' + html_theme = 'default' From 5d011c356dfc659a4c2ef66e8c847d489aef4e9c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 16:12:21 +0200 Subject: [PATCH 111/207] Fixed fallback --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index 932c9bcd..afe6bff3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -264,3 +264,4 @@ except ImportError, e: pygments_style = 'tango' html_theme = 'default' + html_theme_options = {} From 2b103134cf165ba453d5492e1a73b57b1e5bef1e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 17:09:01 +0200 Subject: [PATCH 112/207] Fixed command --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index afe6bff3..50eb8f90 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -259,7 +259,7 @@ except ImportError, e: print 'Warning: Flask themes unavailable. Building with default theme' print 'If you want the Flask themes, run this command and build again:' print - print ' git submodule init' + print ' git submodule update --init' print '-' * 74 pygments_style = 'tango' From 05519a85da1b26d73bc9215aa7f5cd357404f397 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 17:19:10 +0200 Subject: [PATCH 113/207] Fixing submodules --- docs/_themes | 1 + 1 file changed, 1 insertion(+) create mode 160000 docs/_themes diff --git a/docs/_themes b/docs/_themes new file mode 160000 index 00000000..3d964b66 --- /dev/null +++ b/docs/_themes @@ -0,0 +1 @@ +Subproject commit 3d964b660442e23faedf801caed6e3c7bd42d5c9 From 2a0e71f08140fe5c8d32ffed350ba403101f27cc Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 17:22:10 +0200 Subject: [PATCH 114/207] Fixed a typo in an example in the docs --- docs/signals.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/signals.rst b/docs/signals.rst index 7f2d4d97..eeed7343 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -63,7 +63,7 @@ to the template:: recorded.append((template, context)) template_rendered.connect(record) try: - yield templates + yield recorded finally: template_rendered.disconnect(record) From fd06bcfbf03efd496697083666953748ef3fe7a3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 00:51:20 +0200 Subject: [PATCH 115/207] Added notes on 3.x --- docs/installation.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/installation.rst b/docs/installation.rst index 040412c6..a9dc5c28 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -13,6 +13,11 @@ So how do you get all that on your computer quickly? There are many ways which this section will explain, but the most kick-ass method is virtualenv, so let's look at that first. +Either way, you will need Python 2.5 or higher to get started, so be sure +to have an up to date Python 2.x installation. At the time of writing, +the WSGI specification is not yet finalized for Python 3, so Flask cannot +support the 3.x series of Python. + .. _virtualenv: virtualenv From 4a2d2ba3b8259165a97dc80890547f97d7e35ff5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 01:30:28 +0200 Subject: [PATCH 116/207] Added templating docs. This basically fixes #92 --- docs/contents.rst.inc | 1 + docs/quickstart.rst | 3 +- docs/security.rst | 9 ++ docs/templating.rst | 188 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 docs/templating.rst diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index ba2de78f..f32d1da5 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -12,6 +12,7 @@ instructions for web development with Flask. installation quickstart tutorial/index + templating testing errorhandling config diff --git a/docs/quickstart.rst b/docs/quickstart.rst index dc292e8f..d7cf5982 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -379,7 +379,8 @@ package it's actually inside your package: /hello.html For templates you can use the full power of Jinja2 templates. Head over -to the `Jinja2 Template Documentation +to the :ref:`templating` section of the documentation or the official +`Jinja2 Template Documentation `_ for more information. Here an example template: diff --git a/docs/security.rst b/docs/security.rst index 05c9a62c..00f8bc2a 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -5,9 +5,18 @@ Web applications usually face all kinds of security problems and it's very hard to get everything right. Flask tries to solve a few of these things for you, but there are a couple more you have to take care of yourself. +.. _xss: + Cross-Site Scripting (XSS) -------------------------- +Cross site scripting is the concept of injecting arbitrary HTML (and with +it JavaScript) into the context of a website. To rememdy this, developers +have to properly escape text so that it cannot include arbitrary HTML +tags. For more information on that have a look at the Wikipedia article +on `Cross-Site Scripting +`_. + Flask configures Jinja2 to automatically escape all values unless explicitly told otherwise. This should rule out all XSS problems caused in templates, but there are still other places where you have to be diff --git a/docs/templating.rst b/docs/templating.rst new file mode 100644 index 00000000..2583cc2c --- /dev/null +++ b/docs/templating.rst @@ -0,0 +1,188 @@ +Templates +========= + +Flask leverages Jinja2 as template engine. You are obviously free to use +a different template engine, but you still have to install Jinja2 to run +Flask itself. This requirement is necessary to enable rich extensions. +An extension can depend on Jinja2 being present. + +This section only gives a very quick introduction into how Jinja2 +is integrated into Flask. If you want information on the template +engine's syntax itself, head over to the official `Jinja2 Template +Documentation `_ for +more information. + +Jinja Setup +----------- + +Unless customized, Jinja2 is configured by Flask as follows: + +- autoescaping is enabled for all templates ending in ``.html``, + ``.htm``, ``.xml`` as well as ``.xhtml`` +- a template has the ability to opt in/out autoescaping with the + ``{% autoescape %}`` tag. +- Flask inserts a couple of global functions and helpers into the + Jinja2 context, additionally to the values that are present by + default. + +Standard Context +---------------- + +The following global variables are available within Jinja2 templates +by default: + +.. data:: config + :noindex: + + The current configuration object (:data:`flask.config`) + + .. versionadded:: 0.6 + +.. data:: request + :noindex: + + The current request object (:class:`flask.request`) + +.. data:: session + :noindex: + + The current session object (:class:`flask.session`) + +.. data:: g + :noindex: + + The request-bound object for global variables (:data:`flask.g`) + +.. function:: url_for + :noindex: + + The :func:`flask.url_for` function. + +.. function:: get_flashed_messages + :noindex: + + The :func:`flask.get_flashed_messages` function. + +.. admonition:: The Jinja Context Behaviour + + These variables are added to the context of variables, they are not + global variables. The difference is that by default these will not + show up in the context of imported templates. This is partially caused + by performance considerations, partially to keep things explicit. + + What does this mean for you? If you have a macro you want to import, + that needs to access the request object you have two possibilities: + + 1. you explicitly pass the request to the macro as parameter, or + the attribute of the request object you are interested in. + 2. you import the macro "with context". + + Importing with context looks like this: + + .. sourcecode:: jinja + + {% from '_helpers.html' import my_macro with context %} + +Standard Filters +---------------- + +These filters are available in Jinja2 additionally to the filters provided +by Jinja2 itself: + +.. function:: tojson + :noindex: + + This function converts the given object into JSON representation. This + is for example very helpful if you try to generate JavaScript on the + fly. + + Note that inside `script` tags no escaping must take place, so make + sure to disable escaping with ``|safe`` if you intend to use it inside + `script` tags: + + .. sourcecode:: html+jinja + + + + That the ``|tojson`` filter escapes forward slashes properly for you. + +Controlling Autoescaping +------------------------ + +Autoescaping is the concept of automatically escaping special characters +of you. Special characters in the sense of HTML (or XML, and thus XHTML) +are ``&``, ``>``, ``<``, ``"`` as well as ``'``. Because these characters +carry specific meanings in documents on their own you have to replace them +by so called "entities" if you want to use them for text. Not doing so +would not only cause user frustration by the inability to use these +characters in text, but can also lead to security problems. (see +:ref:`xss`) + +Sometimes however you will need to disable autoescaping in templates. +This can be the case if you want to explicitly inject HTML into pages, for +example if they come from a system that generate secure HTML like a +markdown to HTML converter. + +There are three ways to accomplish that: + +- In the Python code, wrap the HTML string in a :class:`~flask.Markup` + object before passing it to the template. This is in general the + recommended way. +- Inside the template, use the ``|safe`` filter to explicitly mark a + string as safe HTML (``{{ myvariable|safe }}``) +- Temporarily disable the autoescape system altogether. + +To disable the autoescape system in templates, you can use the ``{% +autoescape %}`` block: + +.. sourcecode:: html+jinja + + {% autoescape false %} +

autoescaping is disabled here +

{{ will_not_be_escaped }} + {% endautoescape %} + +Whenever you do this, please be very cautious about the varibles you are +using in this block. + +Registering Filters +------------------- + +If you want to register your own filters in Jinja2 you have two ways to do +that. You can either put them by hand into the +:attr:`~flask.Flask.jinja_env` of the application or use the +:meth:`~flask.Flask.template_filter` decorator. + +The two following examples work the same and both reverse an object:: + + @app.template_filter('reverse') + def reverse_filter(s): + return s[::-1] + + def reverse_filter(s): + return s[::-1] + app.jinja_env.filters['reverse'] = reverse_filter + +In case of the decorator the argument is optional if you want to use the +function name as name of the filter. + +Context Processors +------------------ + +To inject new variables automatically into the context of a template +context processors exist in Flask. Context processors run before the +template is rendered and have the ability to inject new values into the +template context. A context processor is a function that returns a +dictionary. The keys and values of this dictionary are then merged with +the template context:: + + @app.context_processor + def inject_user(): + return dict(user=g.user) + +The context processor above makes a variable called `user` available in +the template with the value of `g.user`. This example is not very +interesting because `g` is available in templates anyways, but it gives an +idea how this works. From 0d9b6347d6c77762fb888c067e91e428d65b8601 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 10:30:58 +0200 Subject: [PATCH 117/207] Fixed two typos in the pattern documentation. Thanks Ricky de Laveaga --- docs/patterns/jquery.rst | 2 +- docs/patterns/mongokit.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index a97f7ff4..f2ca39c5 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -40,7 +40,7 @@ Another method is using Google's `AJAX Libraries API In this case you don't have to put jQuery into your static folder, it will instead be loaded from Google directly. This has the advantage that your -website will probably load faster for users if they were to at least one +website will probably load faster for users if they went to at least one other website before using the same jQuery version from Google because it will already be in the browser cache. Downside is that if you don't have network connectivity during development jQuery will not load. diff --git a/docs/patterns/mongokit.rst b/docs/patterns/mongokit.rst index 27f182a3..c1727c80 100644 --- a/docs/patterns/mongokit.rst +++ b/docs/patterns/mongokit.rst @@ -7,7 +7,7 @@ Using a document database rather than a full DBMS gets more common these days. This pattern shows how to use MongoKit, a document mapper library, to integrate with MongoDB. -This pattern requires an running MongoDB server and the MongoKit library +This pattern requires a running MongoDB server and the MongoKit library installed. There are two very common ways to use MongoKit. I will outline each of them From 27d28dcef4cee8765dcd7a567689d35a41ddcd75 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 20:43:50 +0200 Subject: [PATCH 118/207] Added Session again to the public API because existing code might import it --- flask/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flask/__init__.py b/flask/__init__.py index 95497069..ee8508bc 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -23,6 +23,7 @@ from .helpers import url_for, jsonify, json_available, flash, \ from .globals import current_app, g, request, session, _request_ctx_stack from .module import Module from .templating import render_template, render_template_string +from .session import Session # the signals from .signals import signals_available, template_rendered, request_started, \ From 2912ff6f6e85983c372e1bc481337f3fdf6c7b9d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 20:44:07 +0200 Subject: [PATCH 119/207] Added some whitespace --- flask/signals.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flask/signals.py b/flask/signals.py index 17de0447..22447c7c 100644 --- a/flask/signals.py +++ b/flask/signals.py @@ -17,6 +17,7 @@ except ImportError: class Namespace(object): def signal(self, name, doc=None): return _FakeSignal(name, doc) + class _FakeSignal(object): """If blinker is unavailable, create a fake class with the same interface that allows sending of signals but will fail with an From cd4833222eccf7e70380a42b69d5e766a84a6b63 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 20:57:02 +0200 Subject: [PATCH 120/207] Rewrote parts of the foreword and becoming big section --- docs/becomingbig.rst | 36 ++++++++++++++++++++++++------------ docs/foreword.rst | 38 ++++++++++++++++---------------------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst index 916aa324..6c95c6e2 100644 --- a/docs/becomingbig.rst +++ b/docs/becomingbig.rst @@ -18,12 +18,13 @@ expand on that. Flask is designed to be extended and modified in a couple of different ways: +- Flask extensions. For a lot of reusable functionality you can create + extensions. For extensions a number of hooks exist throughout Flask + with signals and callback functions. + - Subclassing. The majority of functionality can be changed by creating a new subclass of the :class:`~flask.Flask` class and overriding - some methods. - -- Flask extensions. For a lot of reusable functionality you can create - extensions. + methods provided for this exact purpose. - Forking. If nothing else works out you can just take the Flask codebase at a given point and copy/paste it into your application @@ -49,8 +50,10 @@ reflected in the license of Flask. You don't have to contribute any changes back if you decide to modify the framework. The downside of forking is of course that Flask extensions will most -likely break because the new framework has a different import name and -because of that forking should be the last resort. +likely break because the new framework has a different import name. +Furthermore integrating upstream changes can be a complex process, +depending on the number of changes. Because of that, forking should be +the very last resort. Scaling like a Pro ------------------ @@ -68,9 +71,18 @@ support a second server. There is only one limiting factor regarding scaling in Flask which are the context local proxies. They depend on context which in Flask is -defined as being either a thread or a greenlet. Separate processes are -fine as well. If your server uses some kind of concurrency that is not -based on threads or greenlets, Flask will no longer be able to support -these global proxies. However the majority of servers are using either -threads, greenlets or separate processes to achieve concurrency which are -all methods well supported by the underlying Werkzeug library. +defined as being either a thread, process or greenlet. If your server +uses some kind of concurrency that is not based on threads or greenlets, +Flask will no longer be able to support these global proxies. However the +majority of servers are using either threads, greenlets or separate +processes to achieve concurrency which are all methods well supported by +the underlying Werkzeug library. + +Dialogue with the Community +--------------------------- + +The Flask developers are very interested to keep everybody happy, so as +soon as you find an obstacle in your way, caused by Flask, don't hesitate +to contact the developers on the mailinglist or IRC channel. The best way +for the Flask and Flask-extension developers to improve it for larger +applications is getting feedback from users. diff --git a/docs/foreword.rst b/docs/foreword.rst index b43fe870..58d47a3b 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -23,11 +23,22 @@ applications because changes on these thread-local objects can happen anywhere in the same thread. Flask provides some tools to deal with the downsides of this approach but -it might be an issue for larger applications. Flask is also based on -convention over configuration, which means that many things are -preconfigured and will work well for smaller applications but not so well -for larger ones. For example, by convention, templates and static files -are in subdirectories within the Python source tree of the application. +it might be an issue for larger applications because in theory +modifications on these objects might happen anywhere in the same thread. + +Flask is also based on convention over configuration, which means that +many things are preconfigured. For example, by convention, templates and +static files are in subdirectories within the Python source tree of the +application. + +The main reason however why Flask is called a "microframework" is the idea +to keep the core simple but extensible. There is database abstraction +layer, no form validation or anything else where different libraries +already exist that can handle that. However Flask knows the concept of +extensions that can add this functionality into your application as if it +was implemented in Flask itself. There are currently extensions for +object relational mappers, form validation, upload handling, various open +authentication technologies and more. However Flask is not much code and built in a very solid foundation and with that very easy to adapt for large applications. If you are @@ -69,20 +80,3 @@ probing for ways to fill your database with spam, links to malicious software, and the like. So always keep security in mind when doing web development. - -Target Audience ---------------- - -Is Flask for you? If your application is small or medium sized and does -not depend on very complex database structures, Flask is the Framework for -you. It was designed from the ground up to be easy to use, and built on -the firm foundation of established principles, good intentions, and -mature, widely used libraries. Recent versions of Flask scale nicely -within reasonable bounds, and if you grow larger, you won't have any -trouble adjusting Flask for your new application size. - -If you suddenly discover that your application grows larger than -originally intended, head over to the :ref:`becomingbig` section to see -some possible solutions for larger applications. - -Satisfied? Then let's proceed with :ref:`installation`. From 9991cfa96b8babe1c59bb4b3548df76e01af07f6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 21:02:56 +0200 Subject: [PATCH 121/207] Added notes for Python 3 --- docs/foreword.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/foreword.rst b/docs/foreword.rst index 58d47a3b..c808307f 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -80,3 +80,27 @@ probing for ways to fill your database with spam, links to malicious software, and the like. So always keep security in mind when doing web development. + +The Status of Python 3 +---------------------- + +Currently the Python community is in the process of improving libraries to +support the new iteration of the Python programming language. +Unfortunately there are a few problems with Python 3, namely the missing +consent on what WSGI for Python 3 should look like. These problems are +partially caused by changes in the language that went unreviewed for too +long, also partially the ambitions of everyone involved to drive the WSGI +standard forward. + +Because of that we strongly recommend against using Python 3 for web +development of any kind and wait until the WSGI situation is resolved. +You will find a couple of frameworks and web libraries on PyPI that claim +Python 3 support, but this support is based on the broken WSGI +implementation provided by Python 3.0 and 3.1 which will most likely +change in the near future. + +Werkzeug and Flask will be ported to Python 3 as soon as a solution for +WSGI is found, and we will provide helpful tips how to upgrade existing +applications to Python 3. Until then, we strongly recommend using Python +2.6 and 2.7 with activated Python 3 warnings during development, as well + as the unicode literals `__future__` feature. From 8837678a088ad4ec710d78de3c30f87be2d13de6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 21:06:46 +0200 Subject: [PATCH 122/207] Changed extensiondev docs --- docs/extensiondev.rst | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 542ec192..3319d229 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -264,9 +264,17 @@ development. If you want to learn more, it's a very good idea to check out existing extensions on the `Flask Extension Registry`_. If you feel lost there is still the `mailinglist`_ and the `IRC channel`_ to get some ideas for nice looking APIs. Especially if you do something nobody before -you did, it might be a very good idea to get some more input. +you did, it might be a very good idea to get some more input. This not +only to get an idea about what people might want to have from an +extension, but also to avoid having multiple developers working on pretty +much the same side by side. -Remember: good API design is hard :( +Remember: good API design is hard, so introduce your project on the +mailinglist, and let other developers give you a helping hand with +designing the API. + +The best Flask extensions are extensions that share common idioms for the +API. And this can only work if collaboration happens early. .. _Flask Extension Wizard: From e269c8eb727bdf72a0ca22e42b895dd37f3670c8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 21:07:45 +0200 Subject: [PATCH 123/207] Removed unused LICENSE file. The doc license is covered by the project's LICENSE file --- docs/LICENSE | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 docs/LICENSE diff --git a/docs/LICENSE b/docs/LICENSE deleted file mode 100644 index af19e1a7..00000000 --- a/docs/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -Copyright (c) 2010 by Armin Ronacher and contributors. See AUTHORS -for more details. - -Some rights reserved. - -Redistribution and use in source (reStructuredText) and 'compiled' forms (HTML, -PDF, PostScript, RTF and so forth) with or without modification, are permitted -provided that the following conditions are met: - -* Redistributions of source code (reStructuredText) must retain the above - copyright notice, this list of conditions and the following disclaimer as the - first lines of this file unmodified. - -* Redistributions in compiled form (converted to HTML, PDF, PostScript, RTF - and so forth) 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. - -THIS DOCUMENTATION 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 -DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From cb660cd1f1a30f67d8ea2b305d635ce573d239c6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 21:08:58 +0200 Subject: [PATCH 124/207] Fixed a typo in the html faq --- docs/htmlfaq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/htmlfaq.rst b/docs/htmlfaq.rst index ca90ae06..1da25f3d 100644 --- a/docs/htmlfaq.rst +++ b/docs/htmlfaq.rst @@ -130,7 +130,7 @@ supported by browsers: - Wrapping the document in an ```` tag - Wrapping header elements in ```` or the body elements in ```` -- Closing the ``

``, ``

  • ``, ``
    ``, ``
    ``, ````, +- Closing the ``

    ``, ``

  • ``, ``
    ``, ``
    ``, ````, ````, ````, ````, ````, or ```` tags. - Quoting attributes, so long as they contain no whitespace or special characters (like ``<``, ``>``, ``'``, or ``"``). From 559d2810d7b6f2938cac402636cc689016f28520 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 21:58:18 +0200 Subject: [PATCH 125/207] Expanded the security docs to mention unquoted attributes as dangerous --- docs/foreword.rst | 2 +- docs/security.rst | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/foreword.rst b/docs/foreword.rst index c808307f..e4c72220 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -103,4 +103,4 @@ Werkzeug and Flask will be ported to Python 3 as soon as a solution for WSGI is found, and we will provide helpful tips how to upgrade existing applications to Python 3. Until then, we strongly recommend using Python 2.6 and 2.7 with activated Python 3 warnings during development, as well - as the unicode literals `__future__` feature. +as the unicode literals `__future__` feature. diff --git a/docs/security.rst b/docs/security.rst index 00f8bc2a..45fff0ca 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -30,6 +30,31 @@ careful: content-type guessing based on the first few bytes so users could trick a browser to execute HTML. +Another thing that is very important are unquoted attributes. While +Jinja2 can protect you from XSS issues by escaping HTML, there is one +thing it cannot protect you from: XSS by attribute injection. To counter +this possible attack vector, be sure to always quote your attributes with +either double or single quotes when using Jinja expressions in them: + +.. sourcecode:: html+jinja + + the text + +Why is this necessary? Because if you would not be doing that, an +attacker could easily inject custom JavaScript handlers. For example an +attacker could inject this piece of HTML+JavaScript: + +.. sourcecode:: html + + onmouseover=alert(document.cookie) + +When the user would then move with the mouse over the link, the cookie +would be presented to the user in an alert window. But instead of showing +the cookie to the user, a good attacker might also execute any other +JavaScript code. In combination with CSS injections the attacker might +even make the element fill out the entire page so that the user would +just have to have the mouse anywhere on the page to trigger the attack. + Cross-Site Request Forgery (CSRF) --------------------------------- From c0abdc4fa5bd63d38974a0c8f28d4b3ba6ca5eb3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 22:01:11 +0200 Subject: [PATCH 126/207] Interlinked design docs better --- docs/design.rst | 6 ++++++ docs/foreword.rst | 3 +++ 2 files changed, 9 insertions(+) diff --git a/docs/design.rst b/docs/design.rst index 20c57a1f..1f391b8c 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -1,3 +1,5 @@ +.. _design: + Design Decisions in Flask ========================= @@ -109,6 +111,10 @@ A template abstraction layer that would not take the unique features of the template engines away is a science on its own and a too large undertaking for a microframework like Flask. +Furthermore extensions can then easily depend on one template language +being present. You can easily use your own templating language, but an +extension could still depend on Jinja itself. + Micro with Dependencies ----------------------- diff --git a/docs/foreword.rst b/docs/foreword.rst index e4c72220..3a7521d4 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -44,6 +44,9 @@ However Flask is not much code and built in a very solid foundation and with that very easy to adapt for large applications. If you are interested in that, check out the :ref:`becomingbig` chapter. +If you are curious about the Flask design principles, head over to the +section about :ref:`design`. + A Framework and an Example -------------------------- From 3b0eb0f3ca45786135cacfc380bfdb51e7744e46 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 20 Jul 2010 10:12:58 +0100 Subject: [PATCH 127/207] Added notes on proxies --- docs/api.rst | 35 +++++++++++++++++++++++++++++++++++ docs/signals.rst | 46 ++++++++++++++++++++++++++++------------------ 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index ca57e8bf..ed2a5037 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -38,6 +38,8 @@ Incoming Request Data sure that you always get the correct data for the active thread if you are in a multithreaded environment. + This is a proxy. See :ref:`notes-on-proxies` for more information. + The request object is an instance of a :class:`~werkzeug.Request` subclass and provides all of the attributes Werkzeug defines. This just shows a quick overview of the most important ones. @@ -164,6 +166,8 @@ To access the current session you can use the :class:`session` object: The session object works pretty much like an ordinary dict, with the difference that it keeps track on modifications. + This is a proxy. See :ref:`notes-on-proxies` for more information. + The following attributes are interesting: .. attribute:: new @@ -206,6 +210,8 @@ thing, like it does for :class:`request` and :class:`session`. Just store on this whatever you want. For example a database connection or the user that is currently logged in. + This is a proxy. See :ref:`notes-on-proxies` for more information. + Useful Functions and Classes ---------------------------- @@ -216,6 +222,8 @@ Useful Functions and Classes extensions that want to support multiple applications running side by side. + This is a proxy. See :ref:`notes-on-proxies` for more information. + .. autofunction:: url_for .. function:: abort(code) @@ -389,6 +397,8 @@ Signals in debug mode, where no exception handling happens. The exception itself is passed to the subscriber as `exception`. +.. currentmodule:: None + .. class:: flask.signals.Namespace An alias for :class:`blinker.base.Namespace` if blinker is available, @@ -404,3 +414,28 @@ Signals operations, including connecting. .. _blinker: http://pypi.python.org/pypi/blinker + +.. _notes-on-proxies: + +Notes On Proxies +---------------- + +Some of the objects provided by Flask are proxies to other objects. The +reason behind this is, that these proxies are shared between threads and +they have to dispatch to the actual object bound to a thread behind the +scenes as necessary. + +Most of the time you don't have to care about that, but there are some +exceptions where it is good to know that this object is an actual proxy: + +- The proxy objects do not fake their inherited types, so if you want to + perform actual instance checks, you have to do that on the instance + that +- if the object reference is important (so for example for sending + :ref:`signals`) + +If you need to get access to the underlying object that is proxied, you +can use the :meth:`~werkzeug.LocalProxy._get_current_object` method:: + + app = current_app._get_current_object() + my_signal.send(app) diff --git a/docs/signals.rst b/docs/signals.rst index eeed7343..513d8694 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -41,13 +41,9 @@ the optional second argument specifies a sender. To unsubscribe from a signal, you can use the :meth:`~blinker.base.Signal.disconnect` method. For all core Flask signals, the sender is the application that issued the -signal. This however might not be true for Flask extensions, so consult -the documentation when subscribing to signals. - -Additionally there is a convenient helper method that allows you to -temporarily subscribe a function to a signal. This is especially helpful -for unittests (:meth:`~blinker.base.Signal.temporarily_connected_to`). -This has to be used in combination with the `with` statement. +signal. When you subscribe to a signal, be sure to also provide a sender +unless you really want to listen for signals of all applications. This is +especially true if you are developing an extension. Here for example a helper context manager that can be used to figure out in a unittest which templates were rendered and what variables were passed @@ -57,19 +53,19 @@ to the template:: from contextlib import contextmanager @contextmanager - def captured_templates(): + def captured_templates(app): recorded = [] def record(template, context): recorded.append((template, context)) - template_rendered.connect(record) + template_rendered.connect(record, app) try: yield recorded finally: - template_rendered.disconnect(record) + template_rendered.disconnect(record, app) This can now easily be paired with a test client:: - with captured_templates() as templates: + with captured_templates(app) as templates: rv = app.test_client().get('/') assert rv.status_code == 200 assert len(templates) == 1 @@ -77,9 +73,23 @@ This can now easily be paired with a test client:: assert template.name == 'index.html' assert len(context['items']) == 10 -All the template rendering in the code, the `with` block wraps will now be -recorded in the `templates` variable. Whenever a template is rendered, -the template object as well as context is appended to it. +All the template rendering in the code issued by the application `app` +in the body of the `with` block will now be recorded in the `templates` +variable. Whenever a template is rendered, the template object as well as +context are appended to it. + +Additionally there is a convenient helper method +(:meth:`~blinker.base.Signal.temporarily_connected_to`). that allows you +to temporarily subscribe a function to a signal with is a context manager +on its own which simplifies the example above:: + + from flask import template_rendered + + def captured_templates(app): + recorded = [] + def record(template, context): + recorded.append((template, context)) + return template_rendered.temporarily_connected_to(record, app) Creating Signals ---------------- @@ -153,7 +163,7 @@ The following signals exist in Flask: context) from flask import request_started - request_started.connect(log_template_renders) + request_started.connect(log_template_renders, app) .. data:: flask.request_started :noindex: @@ -169,7 +179,7 @@ The following signals exist in Flask: sender.logger.debug('Request context is set up') from flask import request_started - request_started.connect(log_request) + request_started.connect(log_request, app) .. data:: flask.request_finished :noindex: @@ -184,7 +194,7 @@ The following signals exist in Flask: 'Response: %s', response) from flask import request_finished - request_finished.connect(log_response) + request_finished.connect(log_response, app) .. data:: flask.got_request_exception :noindex: @@ -200,6 +210,6 @@ The following signals exist in Flask: sender.logger.debug('Got exception during processing: %s', exception) from flask import got_request_exception - got_request_exception.connect(log_exception) + got_request_exception.connect(log_exception, app) .. _blinker: http://pypi.python.org/pypi/blinker From c5b1755317c7bdfc20f4395a9a29a83b5b9cfd43 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 20 Jul 2010 13:48:13 +0100 Subject: [PATCH 128/207] Added testcase for modified URL encodings --- tests/flask_tests.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 43665e1c..698094b9 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -496,6 +496,21 @@ class JSONTestCase(unittest.TestCase): rv = render('{{ "<\0/script>"|tojson|safe }}') assert rv == '"<\\u0000\\/script>"' + def test_modified_url_encoding(self): + class ModifiedRequest(flask.Request): + url_charset = 'euc-kr' + app = flask.Flask(__name__) + app.request_class = ModifiedRequest + app.url_map.charset = 'euc-kr' + + @app.route('/') + def index(): + return flask.request.args['foo'] + + rv = app.test_client().get(u'/?foo=정상처리'.encode('euc-kr')) + assert rv.status_code == 200 + assert rv.data == u'정상처리'.encode('utf-8') + class TemplatingTestCase(unittest.TestCase): From 05108673685c709c9929ab0e76451b74d51fb79b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 20 Jul 2010 15:05:01 +0100 Subject: [PATCH 129/207] Documented blinker 1.1 changes --- docs/signals.rst | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/signals.rst b/docs/signals.rst index 513d8694..feb9a7b2 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -79,9 +79,9 @@ variable. Whenever a template is rendered, the template object as well as context are appended to it. Additionally there is a convenient helper method -(:meth:`~blinker.base.Signal.temporarily_connected_to`). that allows you -to temporarily subscribe a function to a signal with is a context manager -on its own which simplifies the example above:: +(:meth:`~blinker.base.Signal.connected_to`). that allows you to +temporarily subscribe a function to a signal with is a context manager on +its own which simplifies the example above:: from flask import template_rendered @@ -89,7 +89,12 @@ on its own which simplifies the example above:: recorded = [] def record(template, context): recorded.append((template, context)) - return template_rendered.temporarily_connected_to(record, app) + return template_rendered.connected_to(record, app) + +.. admonition:: Blinker API Changes + + The :meth:`~blinker.base.Signal.connected_to` method arrived in Blinker + with version 1.1. Creating Signals ---------------- @@ -141,6 +146,18 @@ function, you can pass ``current_app._get_current_object()`` as sender. that :data:`~flask.current_app` is a proxy and not the real application object. +Decorator Based Signal Subscriptions +------------------------------------ + +With Blinker 1.1 you can also easily subscribe to signals by using the new +:meth:`~blinker.base.NamedSignal.connect_via` decorator:: + + from flask import template_rendered + + @template_rendered.connect_via(app) + def when_template_rendered(template, context): + print 'Template %s is rendered with %s' % (template.name, context) + Core Signals ------------ From 80268a408b981254bbf2bd318aaade9a21a281ef Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 20 Jul 2010 15:05:10 +0100 Subject: [PATCH 130/207] Documented proxy fix --- docs/conf.py | 1 + docs/deploying/others.rst | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 50eb8f90..601edc0a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -246,6 +246,7 @@ intersphinx_mapping = { 'http://werkzeug.pocoo.org/documentation/dev/': None, 'http://www.sqlalchemy.org/docs/': None, 'http://wtforms.simplecodes.com/docs/0.5/': None, + # TODO: push to 1.1 'http://discorporate.us/projects/Blinker/docs/1.0/': None } diff --git a/docs/deploying/others.rst b/docs/deploying/others.rst index 1ec94be4..793a4bed 100644 --- a/docs/deploying/others.rst +++ b/docs/deploying/others.rst @@ -61,3 +61,38 @@ and `greenlet`_. Running a Flask application on this server is quite simple:: .. _eventlet: http://eventlet.net/ .. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html + +Proxy Setups +------------ + +If you deploy your application behind an HTTP proxy you will need to +rewrite a few headers in order for the application to work. The two +problematic values in the WSGI environment usually are `REMOTE_ADDR` and +`HTTP_HOST`. Werkzeug ships a fixer that will solve some common setups, +but you might want to write your own WSGI middlware for specific setups. + +The most common setup invokes the host being set from `X-Forwarded-Host` +and the remote address from `X-Forwared-For`:: + + from werkzeug.contrib.fixers import ProxyFix + app.wsgi_app = ProxyFix(app.wsgi_app) + +Please keep in mind that it is a security issue to use such a middleware +in a non-proxy setup because it will blindly trust the incoming +headers which might be forged by malicious clients. + +If you want to rewrite the headers from another header, you might want to +use a fixer like this:: + + class CustomProxyFix(object): + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + host = environ.get('HTTP_X_FHOST', '') + if host: + environ['HTTP_HOST'] = host + return self.app(environ, start_response) + + app.wsgi_app = CustomProxyFix(app.wsgi_app) From b49afa21ade898061d279ea20ba964d7e583c419 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 20 Jul 2010 15:09:51 +0100 Subject: [PATCH 131/207] Removed temp subscription contextmanager in blinker tests to support upcoming api improvements better --- tests/flask_tests.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 698094b9..5d5c3794 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1052,12 +1052,15 @@ class TestSignals(unittest.TestCase): def record(sender, template, context): recorded.append((template, context)) - with flask.template_rendered.temporarily_connected_to(record, app): + flask.template_rendered.connect(record, app) + try: rv = app.test_client().get('/') assert len(recorded) == 1 template, context = recorded[0] assert template.name == 'simple_template.html' assert context['whiskey'] == 42 + finally: + flask.template_rendered.disconnect(record, app) def test_request_signals(self): app = flask.Flask(__name__) @@ -1110,10 +1113,13 @@ class TestSignals(unittest.TestCase): def record(sender, exception): recorded.append(exception) - with flask.got_request_exception.temporarily_connected_to(record): + flask.got_request_exception.connect(record, app) + try: assert app.test_client().get('/').status_code == 500 assert len(recorded) == 1 assert isinstance(recorded[0], ZeroDivisionError) + finally: + flask.got_request_exception.disconnect(record, app) def suite(): From 7680d52f42a562eda2e3f75b34118abb08d93414 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Jul 2010 13:29:21 +0100 Subject: [PATCH 132/207] Added support for subdomain bound modules --- CHANGES | 3 +++ flask/app.py | 1 + flask/module.py | 12 ++++++++++-- tests/flask_tests.py | 21 +++++++++++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 842c1a9c..c1f50998 100644 --- a/CHANGES +++ b/CHANGES @@ -34,6 +34,9 @@ Release date to be announced, codename to be decided. - refactored the way url adapters are created. This process is now fully customizable with the :meth:`~flask.Flask.create_url_adapter` method. +- modules can now register for a subdomain instead of just an URL + prefix. This makes it possible to bind a whole module to a + configurable subdomain. .. _blinker: http://pypi.python.org/pypi/blinker diff --git a/flask/app.py b/flask/app.py index c5f50135..151525e6 100644 --- a/flask/app.py +++ b/flask/app.py @@ -454,6 +454,7 @@ class Flask(_PackageBoundObject): provided. """ options.setdefault('url_prefix', module.url_prefix) + options.setdefault('subdomain', module.subdomain) state = _ModuleSetupState(self, **options) for func in module._register_events: func(state) diff --git a/flask/module.py b/flask/module.py index 9eaa4f82..e6e1ee36 100644 --- a/flask/module.py +++ b/flask/module.py @@ -37,9 +37,10 @@ def _register_module(module, static_path): class _ModuleSetupState(object): - def __init__(self, app, url_prefix=None): + def __init__(self, app, url_prefix=None, subdomain=None): self.app = app self.url_prefix = url_prefix + self.subdomain = subdomain class Module(_PackageBoundObject): @@ -94,6 +95,9 @@ class Module(_PackageBoundObject): modules to refer to their own templates and static files. See :ref:`modules-and-resources` for more information. + .. versionadded:: 0.6 + The `subdomain` parameter was added. + :param import_name: the name of the Python package or module implementing this :class:`Module`. :param name: the internal short name for the module. Unless specified @@ -101,6 +105,8 @@ class Module(_PackageBoundObject): :param url_prefix: an optional string that is used to prefix all the URL rules of this module. This can also be specified when registering the module with the application. + :param subdomain: used to set the subdomain setting for URL rules that + do not have a subdomain setting set. :param static_path: can be used to specify a different path for the static files on the web. Defaults to ``/static``. This does not affect the folder the files are served @@ -108,7 +114,7 @@ class Module(_PackageBoundObject): """ def __init__(self, import_name, name=None, url_prefix=None, - static_path=None): + static_path=None, subdomain=None): if name is None: assert '.' in import_name, 'name required if package name ' \ 'does not point to a submodule' @@ -116,6 +122,7 @@ class Module(_PackageBoundObject): _PackageBoundObject.__init__(self, import_name) self.name = name self.url_prefix = url_prefix + self.subdomain = subdomain self._register_events = [_register_module(self, static_path)] def route(self, rule, **options): @@ -140,6 +147,7 @@ class Module(_PackageBoundObject): the_rule = rule if state.url_prefix: the_rule = state.url_prefix + rule + options.setdefault('subdomain', state.subdomain) the_endpoint = endpoint if the_endpoint is None: the_endpoint = _endpoint_from_view_func(view_func) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 5d5c3794..3000e41c 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1038,6 +1038,27 @@ class SubdomainTestCase(unittest.TestCase): rv = c.get('/', 'http://mitsuhiko.localhost/') assert rv.data == 'index for mitsuhiko' + def test_module_subdomain_support(self): + app = flask.Flask(__name__) + mod = flask.Module(__name__, 'test', subdomain='testing') + app.config['SERVER_NAME'] = 'localhost' + + @mod.route('/test') + def test(): + return 'Test' + + @mod.route('/outside', subdomain='xtesting') + def bar(): + return 'Outside' + + app.register_module(mod) + + c = app.test_client() + rv = c.get('/test', 'http://testing.localhost/') + assert rv.data == 'Test' + rv = c.get('/outside', 'http://xtesting.localhost/') + assert rv.data == 'Outside' + class TestSignals(unittest.TestCase): From dcf5dff414f2f4b9b24047510d3fa7ac643e971f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Jul 2010 14:58:51 +0100 Subject: [PATCH 133/207] Updatd an intersphinx link --- docs/conf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 601edc0a..3be35fb6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -246,8 +246,7 @@ intersphinx_mapping = { 'http://werkzeug.pocoo.org/documentation/dev/': None, 'http://www.sqlalchemy.org/docs/': None, 'http://wtforms.simplecodes.com/docs/0.5/': None, - # TODO: push to 1.1 - 'http://discorporate.us/projects/Blinker/docs/1.0/': None + 'http://discorporate.us/projects/Blinker/docs/1.1/': None } pygments_style = 'flask_theme_support.FlaskyStyle' From 3adc9de5ec39e65f64bcd684613c64b29131a64e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Jul 2010 14:59:10 +0100 Subject: [PATCH 134/207] Updated docs for extension approval process --- docs/extensiondev.rst | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 3319d229..70cfbeb7 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -277,6 +277,46 @@ The best Flask extensions are extensions that share common idioms for the API. And this can only work if collaboration happens early. +Approved Extensions +------------------- + +Flask also has the concept of approved extensions. Approved extensions +are tested as part of Flask itself to ensure extensions do not break on +new releases. These approved extensions are listed on the `Flask +Extension Registry`_ and marked appropriately. If you want your own +extension to be approved you have to follow these guidelines: + +1. An approved Flask extension must provide exactly one package or module + inside the `flaskext` namespace package. +2. It must ship a testsuite that can either be invoked with ``make test`` + or ``python setup.py test``. For testsuites invoked with ``make + test`` the extension has to ensure that all dependencies for the test + are installed automatically, in case of ``python setup.py test`` + dependencies for tests alone can be specified in the `setup.py` + file. The testsuite also has to be part of the distribution. +3. APIs of approved extensions will be checked for the following + behavioristics: + + - an approved extension has to support multiple applications + running in the same Python process. + - it must be possible to use the factory pattern for creating + applications. + +4. The license has to be BSD/MIT/WTFPL licensed unless a depending + library absolutely enforces GPL or another license. +5. The naming scheme for official extensions is *Flask-ExtensionName* or + *ExtensionName-Flask*. +6. Approved extensions must define all their dependencies in the + `setup.py` file unless a dependency cannot by met because it is not + available on PyPI. +7. The extension must have documentation that furthermore uses one of + the two Flask themes for Sphinx documentation. +8. The setup.py description (and thus the PyPI description) has to + link to the documentation, website (if there is one) and there + must be a link to automatically install the development version + (``PackageName==dev``). + + .. _Flask Extension Wizard: http://github.com/mitsuhiko/flask-extension-wizard .. _OAuth extension: http://packages.python.org/Flask-OAuth/ From 66c13955895c7afb849513d9d0ff0f7f8361c511 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Jul 2010 15:21:08 +0100 Subject: [PATCH 135/207] Fixed wording --- docs/extensiondev.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 70cfbeb7..a22b5855 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -309,8 +309,8 @@ extension to be approved you have to follow these guidelines: 6. Approved extensions must define all their dependencies in the `setup.py` file unless a dependency cannot by met because it is not available on PyPI. -7. The extension must have documentation that furthermore uses one of - the two Flask themes for Sphinx documentation. +7. The extension must have documentation that uses one of the two Flask + themes for Sphinx documentation. 8. The setup.py description (and thus the PyPI description) has to link to the documentation, website (if there is one) and there must be a link to automatically install the development version From f4bfae622b5515ebde795fb71e3cc128cc0d5ead Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Jul 2010 15:41:39 +0100 Subject: [PATCH 136/207] Added flaskext tester --- tests/flaskext_test.py | 97 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 tests/flaskext_test.py diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py new file mode 100644 index 00000000..aa4b7461 --- /dev/null +++ b/tests/flaskext_test.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +""" + Flask Extension Tests + ~~~~~~~~~~~~~~~~~~~~~ + + Tests the Flask extensions. + + :copyright: (c) 2010 by Ali Afshar. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import tempfile, subprocess, urllib2, os + +from flask import json + +from setuptools.package_index import PackageIndex +from setuptools.archive_util import unpack_archive + +flask_svc_url = 'http://flask.pocoo.org/extensions/' +tdir = tempfile.mkdtemp() + + +def run_tests(checkout_dir): + cmd = ['tox'] + return subprocess.call(cmd, cwd=checkout_dir, + stdout=open(os.path.join(tdir, 'tox.log'), 'w'), + stderr=subprocess.STDOUT) + + +def get_test_command(checkout_dir): + files = set(os.listdir(checkout_dir)) + if 'Makefile' in files: + return 'make test' + elif 'conftest.py' in files: + return 'py.test' + else: + return 'nosetests' + + +def fetch_extensions_list(): + req = urllib2.Request(flask_svc_url, headers={'accept':'application/json'}) + d = urllib2.urlopen(req).read() + data = json.loads(d) + for ext in data['extensions']: + yield ext + + +def checkout_extension(ext): + name = ext['name'] + root = os.path.join(tdir, name) + os.mkdir(root) + checkout_path = PackageIndex().download(ext['name'], root) + unpack_archive(checkout_path, root) + path = None + for fn in os.listdir(root): + path = os.path.join(root, fn) + if os.path.isdir(path): + break + return path + + +tox_template = """[tox] +envlist=py26 + +[testenv] +commands= +%s +downloadcache= +%s +""" + +def create_tox_ini(checkout_path): + tox_path = os.path.join(checkout_path, 'tox.ini') + if not os.path.exists(tox_path): + with open(tox_path, 'w') as f: + f.write(tox_template % (get_test_command(checkout_path), tdir)) + +# XXX command line +only_approved = True + +def test_all_extensions(only_approved=only_approved): + for ext in fetch_extensions_list(): + if ext['approved'] or not only_approved: + checkout_path = checkout_extension(ext) + create_tox_ini(checkout_path) + ret = run_tests(checkout_path) + yield ext['name'], ret + +def main(): + for name, ret in test_all_extensions(): + print name, ret + + +if __name__ == '__main__': + main() From cb79b58053c34dc222c84584da506650ec64ee6f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Jul 2010 15:41:58 +0100 Subject: [PATCH 137/207] Added ali to authors --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index c2415977..936b0b0f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -10,6 +10,7 @@ Patches and Suggestions ``````````````````````` - Adam Zapletal +- Ali Afshar - Chris Edgemon - Chris Grindstaff - Christopher Grebs From b2fb2245f1a115c65cba42c6c34f0eae30da4c6c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Jul 2010 15:42:33 +0100 Subject: [PATCH 138/207] Fixed a word in the docs --- docs/extensiondev.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index a22b5855..9eb2dbbe 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -295,7 +295,7 @@ extension to be approved you have to follow these guidelines: dependencies for tests alone can be specified in the `setup.py` file. The testsuite also has to be part of the distribution. 3. APIs of approved extensions will be checked for the following - behavioristics: + characteristics: - an approved extension has to support multiple applications running in the same Python process. From 63114529ed51213832c9ada0b6216ec9e8e22052 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Jul 2010 17:49:03 +0100 Subject: [PATCH 139/207] good -> bad --- docs/tutorial/testing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/testing.rst b/docs/tutorial/testing.rst index ed9fc3e3..34edd791 100644 --- a/docs/tutorial/testing.rst +++ b/docs/tutorial/testing.rst @@ -4,7 +4,7 @@ Bonus: Testing the Application ============================== Now that you have finished the application and everything works as -expected, it's probably not a good idea to add automated tests to simplify +expected, it's probably not a bad idea to add automated tests to simplify modifications in the future. The application above is used as a basic example of how to perform unittesting in the :ref:`testing` section of the documentation. Go there to see how easy it is to test Flask applications. From a7d83a9f8008a58813e4495dd8b00d14214a51ad Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 14:59:31 +0200 Subject: [PATCH 140/207] Approved extensions must not be GPL --- docs/extensiondev.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 9eb2dbbe..e181fe3e 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -302,12 +302,11 @@ extension to be approved you have to follow these guidelines: - it must be possible to use the factory pattern for creating applications. -4. The license has to be BSD/MIT/WTFPL licensed unless a depending - library absolutely enforces GPL or another license. +4. The license must be BSD/MIT/WTFPL licensed. 5. The naming scheme for official extensions is *Flask-ExtensionName* or *ExtensionName-Flask*. 6. Approved extensions must define all their dependencies in the - `setup.py` file unless a dependency cannot by met because it is not + `setup.py` file unless a dependency cannot be met because it is not available on PyPI. 7. The extension must have documentation that uses one of the two Flask themes for Sphinx documentation. From a993a314fe7be148747cf804167d45f2ea8bf18e Mon Sep 17 00:00:00 2001 From: Ali Afshar Date: Sun, 25 Jul 2010 18:45:10 +0800 Subject: [PATCH 141/207] Removed unused import --- flask/templating.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/templating.py b/flask/templating.py index 41c0d39b..db78c3af 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -8,7 +8,7 @@ :copyright: (c) 2010 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ -from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound +from jinja2 import BaseLoader, TemplateNotFound from .globals import _request_ctx_stack from .signals import template_rendered From 3a80ecc66057d97e3a0ceb40e99ea7bde035381b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 17:33:45 +0200 Subject: [PATCH 142/207] Improved script for automatic extension testing --- Makefile | 5 +- tests/flaskext_test.py | 244 +++++++++++++++++++++++++++++++++++------ 2 files changed, 216 insertions(+), 33 deletions(-) diff --git a/Makefile b/Makefile index ba898b9e..8055e8ef 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,13 @@ -.PHONY: clean-pyc test upload-docs docs +.PHONY: clean-pyc ext-test test upload-docs docs all: clean-pyc test test: python setup.py test +ext-test: + python tests/flaskext_test.py --browse + release: python setup.py release sdist upload diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py index aa4b7461..3614ad58 100644 --- a/tests/flaskext_test.py +++ b/tests/flaskext_test.py @@ -11,7 +11,14 @@ from __future__ import with_statement -import tempfile, subprocess, urllib2, os +import os +import sys +import shutil +import urllib2 +import tempfile +import subprocess +import argparse +from cStringIO import StringIO from flask import json @@ -19,24 +26,136 @@ from setuptools.package_index import PackageIndex from setuptools.archive_util import unpack_archive flask_svc_url = 'http://flask.pocoo.org/extensions/' -tdir = tempfile.mkdtemp() + +if sys.platform == 'darwin': + _tempdir = '/private/tmp' +else: + _tempdir = tempfile.gettempdir() +tdir = _tempdir + '/flaskext-test' +flaskdir = os.path.abspath(os.path.join(os.path.dirname(__file__), + '..')) -def run_tests(checkout_dir): - cmd = ['tox'] - return subprocess.call(cmd, cwd=checkout_dir, - stdout=open(os.path.join(tdir, 'tox.log'), 'w'), - stderr=subprocess.STDOUT) +RESULT_TEMPATE = u'''\ + +Flask-Extension Test Results + +

    Flask-Extension Test Results

    +

    + This page contains the detailed test results for the test run of + all {{ 'approved' if approved }} Flask extensions. +

    Summary

    + + + + + + + {%- for result in results %} + {% set outcome = 'success' if result.success else 'failed' %} + + + {%- endfor %} + +
    Extension + Version + Author + License + Outcome +
    {{ result.name }} + {{ result.version }} + {{ result.author }} + {{ result.license }} + {{ outcome }} +
    +

    Test Logs

    +

    Detailed test logs for all tests on all platforms: +{%- for result in results %} + {%- for iptr, log in result.logs|dictsort %} +

    {{ result.name }} - {{ result.version }} [{{ iptr }}]

    +
    {{ log }}
    + {%- endfor %} +{%- endfor %} +''' + + +def log(msg, *args): + print '[EXTTEST]', msg % args + + +class TestResult(object): + + def __init__(self, name, folder, statuscode, interpreters): + intrptr = os.path.join(folder, '.tox/%s/bin/python' + % interpreters[0]) + self.statuscode = statuscode + self.folder = folder + self.success = statuscode == 0 + + def fetch(field): + try: + c = subprocess.Popen([intrptr, 'setup.py', + '--' + field], cwd=folder, + stdout=subprocess.PIPE) + return c.communicate()[0].strip() + except OSError: + return '?' + self.name = name + self.license = fetch('license') + self.author = fetch('author') + self.version = fetch('version') + + self.logs = {} + for interpreter in interpreters: + logfile = os.path.join(folder, '.tox/%s/log/test.log' + % interpreter) + if os.path.isfile(logfile): + self.logs[interpreter] = open(logfile).read() + else: + self.logs[interpreter] = '' + + +def create_tdir(): + try: + shutil.rmtree(tdir) + except Exception: + pass + os.mkdir(tdir) + + +def package_flask(): + distfolder = tdir + '/.flask-dist' + c = subprocess.Popen(['python', 'setup.py', 'sdist', '--formats=gztar', + '--dist', distfolder], cwd=flaskdir) + c.wait() + return os.path.join(distfolder, os.listdir(distfolder)[0]) def get_test_command(checkout_dir): - files = set(os.listdir(checkout_dir)) - if 'Makefile' in files: + if os.path.isfile(checkout_dir + '/Makefile'): return 'make test' - elif 'conftest.py' in files: - return 'py.test' - else: - return 'nosetests' + return 'python setup.py test' def fetch_extensions_list(): @@ -47,50 +166,111 @@ def fetch_extensions_list(): yield ext -def checkout_extension(ext): - name = ext['name'] +def checkout_extension(name): + log('Downloading extension %s to temporary folder', name) root = os.path.join(tdir, name) os.mkdir(root) - checkout_path = PackageIndex().download(ext['name'], root) + checkout_path = PackageIndex().download(name, root) + unpack_archive(checkout_path, root) path = None for fn in os.listdir(root): path = os.path.join(root, fn) if os.path.isdir(path): break + log('Downloaded to %s', path) return path tox_template = """[tox] -envlist=py26 +envlist=%(env)s [testenv] -commands= -%s -downloadcache= -%s +deps=%(deps)s +commands=bash flaskext-runtest.sh {envlogdir}/test.log +downloadcache=%(cache)s """ -def create_tox_ini(checkout_path): + +def create_tox_ini(checkout_path, interpreters, flask_dep): tox_path = os.path.join(checkout_path, 'tox.ini') if not os.path.exists(tox_path): with open(tox_path, 'w') as f: - f.write(tox_template % (get_test_command(checkout_path), tdir)) + f.write(tox_template % { + 'env': ','.join(interpreters), + 'cache': tdir, + 'deps': flask_dep + }) -# XXX command line -only_approved = True -def test_all_extensions(only_approved=only_approved): +def iter_extensions(only_approved=True): for ext in fetch_extensions_list(): if ext['approved'] or not only_approved: - checkout_path = checkout_extension(ext) - create_tox_ini(checkout_path) - ret = run_tests(checkout_path) - yield ext['name'], ret + yield ext['name'] + + +def test_extension(name, interpreters, flask_dep): + checkout_path = checkout_extension(name) + log('Running tests with tox in %s', checkout_path) + + # figure out the test command and write a wrapper script. We + # can't write that directly into the tox ini because tox does + # not invoke the command from the shell so we have no chance + # to pipe the output into a logfile + test_command = get_test_command(checkout_path) + log('Test command: %s', test_command) + f = open(checkout_path + '/flaskext-runtest.sh', 'w') + f.write(test_command + ' &> "$1"\n') + f.close() + + create_tox_ini(checkout_path, interpreters, flask_dep) + rv = subprocess.call(['tox'], cwd=checkout_path) + return TestResult(name, checkout_path, rv, interpreters) + + +def run_tests(interpreters, only_approved=True): + results = {} + create_tdir() + log('Packaging Flask') + flask_dep = package_flask() + log('Running extension tests') + log('Temporary Environment: %s', tdir) + for name in iter_extensions(only_approved): + log('Testing %s', name) + result = test_extension(name, interpreters, flask_dep) + if result.success: + log('Extension test succeeded') + else: + log('Extension test failed') + results[name] = result + return results + + +def render_results(results, approved): + from jinja2 import Template + items = results.values() + items.sort(key=lambda x: x.name.lower()) + rv = Template(RESULT_TEMPATE, autoescape=True).render(results=items, + approved=approved) + fd, filename = tempfile.mkstemp(suffix='.html') + os.fdopen(fd, 'w').write(rv.encode('utf-8') + '\n') + return filename + def main(): - for name, ret in test_all_extensions(): - print name, ret + parser = argparse.ArgumentParser(description='Runs Flask extension tests') + parser.add_argument('--all', dest='all', action='store_true', + help='run against all extensions, not just approved') + parser.add_argument('--browse', dest='browse', action='store_true', + help='show browser with the result summary') + args = parser.parse_args() + + results = run_tests(['py26'], not args.all) + filename = render_results(results, not args.all) + if args.browse: + import webbrowser + webbrowser.open('file:///' + filename.lstrip('/')) + print 'Results written to', filename if __name__ == '__main__': From fee5bdafe84bfcec492c2643544373c71f1e82a8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 17:34:47 +0200 Subject: [PATCH 143/207] Code cleanup --- flask/app.py | 3 +-- flask/helpers.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/flask/app.py b/flask/app.py index 151525e6..9b6b7836 100644 --- a/flask/app.py +++ b/flask/app.py @@ -11,7 +11,6 @@ from __future__ import with_statement -import os from threading import Lock from datetime import timedelta, datetime from itertools import chain @@ -20,7 +19,7 @@ from jinja2 import Environment from werkzeug import ImmutableDict from werkzeug.routing import Map, Rule -from werkzeug.exceptions import HTTPException, InternalServerError, NotFound +from werkzeug.exceptions import HTTPException, InternalServerError from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ _tojson_filter, _endpoint_from_view_func diff --git a/flask/helpers.py b/flask/helpers.py index 39214a10..a2d951b4 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -15,7 +15,6 @@ import posixpath import mimetypes from time import time from zlib import adler32 -from functools import wraps # try to load the best simplejson implementation available. If JSON # is not installed, we add a failing class. From 312dfb43735dfaf063f58f8115c61331990302f6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 18:51:28 +0200 Subject: [PATCH 144/207] Mentioned zip_safe --- docs/extensiondev.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index e181fe3e..c2a6d2f7 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -314,6 +314,8 @@ extension to be approved you have to follow these guidelines: link to the documentation, website (if there is one) and there must be a link to automatically install the development version (``PackageName==dev``). +9. The ``zip_safe`` flag in the setup scrip must be set to ``False``, + even if the extension would be safe for zipping. .. _Flask Extension Wizard: From 9de61ea980edbd8fc3e2c459d3276b8f23d1428c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 18:51:36 +0200 Subject: [PATCH 145/207] Reviewed all extensions and wrote down notes. --- extreview/approved.rst | 32 +++++++ extreview/listed.rst | 183 +++++++++++++++++++++++++++++++++++++++++ extreview/rejected.rst | 55 +++++++++++++ 3 files changed, 270 insertions(+) create mode 100644 extreview/approved.rst create mode 100644 extreview/listed.rst create mode 100644 extreview/rejected.rst diff --git a/extreview/approved.rst b/extreview/approved.rst new file mode 100644 index 00000000..852374db --- /dev/null +++ b/extreview/approved.rst @@ -0,0 +1,32 @@ +Approved Extensions +=================== + +This document contains a list of all extensions that were approved and the +date of approval as well as notes. This should make it possible to better +track the extension approval process. + + +Flask-Babel +----------- + +First Approval: 2010-07-23 +Last Review: 2010-07-23 +Approved version: 0.6 +Approved license: BSD + +Notes: Developed by the Flask development head + +How to improve: add a better long description to the next release + + +Flask-SQLAlchemy +---------------- + +First Approval: 2010-07-25 +Last Review: 2010-07-25 +Approved version: 0.9 +Approved license: BSD + +Notes: Developed by the Flask development head + +How to improve: add a better long description to the next release diff --git a/extreview/listed.rst b/extreview/listed.rst new file mode 100644 index 00000000..426e6ce4 --- /dev/null +++ b/extreview/listed.rst @@ -0,0 +1,183 @@ +Listed Extensions +================= + +This list contains extensions that passed listing. This means the +extension is on the list of extensions on the website. It does not +contain extensions that are approved. + + +Flask-CouchDB +------------- + +Last-Review: 2010-07-25 +Reviewed version: 0.2 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". + + +Flask-CouchDBKit +---------------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.2 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". + + +Flask-Creole +------------ + +Last-Review: 2010-07-25 +Reviewed Version: 0.2 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". Furthermore the README +file is empty. + + +flask-csrf +---------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.2 + +Will not be approved because this is functionality that should be handled +in the form handling systems which is for Flask-WTF already the case. +Also, this implementation only supports one open tab with forms. + +Name is not following Flask extension naming rules. + +Considered for unlisting. + + +Flask-Genshi +------------ + +Last-Review: 2010-07-25 +Reviewed Version: 0.3 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". Furthermore the long +description is empty. The zip_safe flag is not set to False which is a +requirement for approved extensions. + + +flask-lesscss +------------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.9.1 + +Broken package description, nonconforming package name, does not follow +standard API rules (init_lesscss instead of lesscss). + +Considered for unlisting, improved version should release as +"Flask-LessCSS" with a conforming API and fixed packages indices, as well +as a testsuite. + + +flask-mail +---------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.3.1 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". Furthermore the long +description in the package index is a little bit too short. + +Package name should be changed to Flask-Mail with the approval to be +consistent, this might also be the change to improve the API if necessary, +but I don't see any big design problems there. + + +Flask-OAuth +----------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.9 + +Short long description, missing tests. + + +Flask-OpenID +------------ + +Last-Review: 2010-07-25 +Reviewed Version: 1.0.1 + +Short long description, missing tests. + + +Flask-Script +------------ + +Last-Review: 2010-07-25 +Reviewed Version: 0.2 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". + +The upcoming 0.3 release looks promising, could need a longer "long +description" in the package index though. + + +Flask-Testing +------------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.2 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". + + +Flask-Themes +------------ + +Last-Review: 2010-07-25 +Reviewed Version: 0.1 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". + + +Flask-Uploads +------------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.1 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". + + +Flask-WTF +--------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.2.1 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". + + +Flask-XML-RPC +------------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.2.1 + +Missing tests, API wise it would be fine for approval. diff --git a/extreview/rejected.rst b/extreview/rejected.rst new file mode 100644 index 00000000..a953f363 --- /dev/null +++ b/extreview/rejected.rst @@ -0,0 +1,55 @@ +Rejected Extensions +=================== + +This is a list of extensions that is currently rejected from listing and +with that also not approved. If an extension ends up here it should +improved to be listed. + + +Flask-Actions +------------- + +Last Review: 2010-07-25 +Reviewed version: 0.2 + +Rejected because of missing description in PyPI, formatting issues with +the documentation (missing headlines, scrollbars etc.) and a general clash +of functionality with the Flask-Script package. Latter should not be a +problem, but the documentation should improve. For listing, the extension +developer should probably discuss the extension on the mailinglist with +others. + +Futhermore it also has an egg registered with an invalid filename. + + +Flask-Jinja2Extender +-------------------- + +Last Review: 2010-07-25 +Reviewed version: 0.1 + +Usecase not obvious, hacky implementation, does not solve a problem that +could not be solved with Flask itself. I suppose it is to aid other +extensions, but that should be discussed on the mailinglist. + + +Flask-Markdown +-------------- + +Last Review: 2010-07-25 +Reviewed version: 0.2 + +Would be great for enlisting but it should follow the API of Flask-Creole. +Besides that, the docstrings are not valid rst (run through rst2html to +see the issue) and it is missing tests. Otherwise fine :) + + +flask-urls +---------- + +Last Review: 2010-07-25 +Reviewed version: 0.9.2 + +Broken PyPI index and non-conforming extension name. Due to the small +featureset this was also delisted from the list. It was there previously +before the approval process was introduced. From 4c5c644fbf8e94096eb1a61d76855a98687cdb89 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 18:56:14 +0200 Subject: [PATCH 146/207] Renamed rejected to unlisted --- extreview/{rejected.rst => unlisted.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename extreview/{rejected.rst => unlisted.rst} (100%) diff --git a/extreview/rejected.rst b/extreview/unlisted.rst similarity index 100% rename from extreview/rejected.rst rename to extreview/unlisted.rst From 2401d86008bb68f5f0368fbfe8d5797a8ef75903 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 19:00:00 +0200 Subject: [PATCH 147/207] Fixed formatting --- extreview/approved.rst | 16 +++++------ extreview/listed.rst | 60 +++++++++++++++++++++--------------------- extreview/unlisted.rst | 18 ++++++------- 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/extreview/approved.rst b/extreview/approved.rst index 852374db..a09aeb11 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -9,10 +9,10 @@ track the extension approval process. Flask-Babel ----------- -First Approval: 2010-07-23 -Last Review: 2010-07-23 -Approved version: 0.6 -Approved license: BSD +:First Approval: 2010-07-23 +:Last Review: 2010-07-23 +:Approved version: 0.6 +:Approved license: BSD Notes: Developed by the Flask development head @@ -22,10 +22,10 @@ How to improve: add a better long description to the next release Flask-SQLAlchemy ---------------- -First Approval: 2010-07-25 -Last Review: 2010-07-25 -Approved version: 0.9 -Approved license: BSD +:First Approval: 2010-07-25 +:Last Review: 2010-07-25 +:Approved version: 0.9 +:Approved license: BSD Notes: Developed by the Flask development head diff --git a/extreview/listed.rst b/extreview/listed.rst index 426e6ce4..cd658e92 100644 --- a/extreview/listed.rst +++ b/extreview/listed.rst @@ -9,8 +9,8 @@ contain extensions that are approved. Flask-CouchDB ------------- -Last-Review: 2010-07-25 -Reviewed version: 0.2 +:Last-Review: 2010-07-25 +:Reviewed version: 0.2 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -20,8 +20,8 @@ to either "make test" or "python setup.py test". Flask-CouchDBKit ---------------- -Last-Review: 2010-07-25 -Reviewed Version: 0.2 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.2 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -31,8 +31,8 @@ to either "make test" or "python setup.py test". Flask-Creole ------------ -Last-Review: 2010-07-25 -Reviewed Version: 0.2 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.2 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -43,8 +43,8 @@ file is empty. flask-csrf ---------- -Last-Review: 2010-07-25 -Reviewed Version: 0.2 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.2 Will not be approved because this is functionality that should be handled in the form handling systems which is for Flask-WTF already the case. @@ -58,8 +58,8 @@ Considered for unlisting. Flask-Genshi ------------ -Last-Review: 2010-07-25 -Reviewed Version: 0.3 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.3 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -71,8 +71,8 @@ requirement for approved extensions. flask-lesscss ------------- -Last-Review: 2010-07-25 -Reviewed Version: 0.9.1 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.9.1 Broken package description, nonconforming package name, does not follow standard API rules (init_lesscss instead of lesscss). @@ -85,8 +85,8 @@ as a testsuite. flask-mail ---------- -Last-Review: 2010-07-25 -Reviewed Version: 0.3.1 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.3.1 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -101,8 +101,8 @@ but I don't see any big design problems there. Flask-OAuth ----------- -Last-Review: 2010-07-25 -Reviewed Version: 0.9 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.9 Short long description, missing tests. @@ -110,8 +110,8 @@ Short long description, missing tests. Flask-OpenID ------------ -Last-Review: 2010-07-25 -Reviewed Version: 1.0.1 +:Last-Review: 2010-07-25 +:Reviewed Version: 1.0.1 Short long description, missing tests. @@ -119,8 +119,8 @@ Short long description, missing tests. Flask-Script ------------ -Last-Review: 2010-07-25 -Reviewed Version: 0.2 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.2 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -133,8 +133,8 @@ description" in the package index though. Flask-Testing ------------- -Last-Review: 2010-07-25 -Reviewed Version: 0.2 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.2 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -144,8 +144,8 @@ to either "make test" or "python setup.py test". Flask-Themes ------------ -Last-Review: 2010-07-25 -Reviewed Version: 0.1 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.1 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -155,8 +155,8 @@ to either "make test" or "python setup.py test". Flask-Uploads ------------- -Last-Review: 2010-07-25 -Reviewed Version: 0.1 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.1 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -166,8 +166,8 @@ to either "make test" or "python setup.py test". Flask-WTF --------- -Last-Review: 2010-07-25 -Reviewed Version: 0.2.1 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.2.1 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -177,7 +177,7 @@ to either "make test" or "python setup.py test". Flask-XML-RPC ------------- -Last-Review: 2010-07-25 -Reviewed Version: 0.2.1 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.2.1 Missing tests, API wise it would be fine for approval. diff --git a/extreview/unlisted.rst b/extreview/unlisted.rst index a953f363..6168c912 100644 --- a/extreview/unlisted.rst +++ b/extreview/unlisted.rst @@ -1,4 +1,4 @@ -Rejected Extensions +Unlisted Extensions =================== This is a list of extensions that is currently rejected from listing and @@ -9,8 +9,8 @@ improved to be listed. Flask-Actions ------------- -Last Review: 2010-07-25 -Reviewed version: 0.2 +:Last Review: 2010-07-25 +:Reviewed version: 0.2 Rejected because of missing description in PyPI, formatting issues with the documentation (missing headlines, scrollbars etc.) and a general clash @@ -25,8 +25,8 @@ Futhermore it also has an egg registered with an invalid filename. Flask-Jinja2Extender -------------------- -Last Review: 2010-07-25 -Reviewed version: 0.1 +:Last Review: 2010-07-25 +:Reviewed version: 0.1 Usecase not obvious, hacky implementation, does not solve a problem that could not be solved with Flask itself. I suppose it is to aid other @@ -36,8 +36,8 @@ extensions, but that should be discussed on the mailinglist. Flask-Markdown -------------- -Last Review: 2010-07-25 -Reviewed version: 0.2 +:Last Review: 2010-07-25 +:Reviewed version: 0.2 Would be great for enlisting but it should follow the API of Flask-Creole. Besides that, the docstrings are not valid rst (run through rst2html to @@ -47,8 +47,8 @@ see the issue) and it is missing tests. Otherwise fine :) flask-urls ---------- -Last Review: 2010-07-25 -Reviewed version: 0.9.2 +:Last Review: 2010-07-25 +:Reviewed version: 0.9.2 Broken PyPI index and non-conforming extension name. Due to the small featureset this was also delisted from the list. It was there previously From c6f55b5dbd1c5f08e56aa13b61b9b9912b5b4f21 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 19:02:02 +0200 Subject: [PATCH 148/207] Consistency --- extreview/approved.rst | 8 ++++---- extreview/unlisted.rst | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/extreview/approved.rst b/extreview/approved.rst index a09aeb11..ea178ceb 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -11,8 +11,8 @@ Flask-Babel :First Approval: 2010-07-23 :Last Review: 2010-07-23 -:Approved version: 0.6 -:Approved license: BSD +:Approved Version: 0.6 +:Approved License: BSD Notes: Developed by the Flask development head @@ -24,8 +24,8 @@ Flask-SQLAlchemy :First Approval: 2010-07-25 :Last Review: 2010-07-25 -:Approved version: 0.9 -:Approved license: BSD +:Approved Version: 0.9 +:Approved License: BSD Notes: Developed by the Flask development head diff --git a/extreview/unlisted.rst b/extreview/unlisted.rst index 6168c912..6ee8b441 100644 --- a/extreview/unlisted.rst +++ b/extreview/unlisted.rst @@ -10,7 +10,7 @@ Flask-Actions ------------- :Last Review: 2010-07-25 -:Reviewed version: 0.2 +:Reviewed Version: 0.2 Rejected because of missing description in PyPI, formatting issues with the documentation (missing headlines, scrollbars etc.) and a general clash @@ -26,7 +26,7 @@ Flask-Jinja2Extender -------------------- :Last Review: 2010-07-25 -:Reviewed version: 0.1 +:Reviewed Version: 0.1 Usecase not obvious, hacky implementation, does not solve a problem that could not be solved with Flask itself. I suppose it is to aid other @@ -37,7 +37,7 @@ Flask-Markdown -------------- :Last Review: 2010-07-25 -:Reviewed version: 0.2 +:Reviewed Version: 0.2 Would be great for enlisting but it should follow the API of Flask-Creole. Besides that, the docstrings are not valid rst (run through rst2html to @@ -48,7 +48,7 @@ flask-urls ---------- :Last Review: 2010-07-25 -:Reviewed version: 0.9.2 +:Reviewed Version: 0.9.2 Broken PyPI index and non-conforming extension name. Due to the small featureset this was also delisted from the list. It was there previously From 385e67422ba1d590cf65e81dc7a85c916ada62e8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 19:44:08 +0200 Subject: [PATCH 149/207] Reviewed 0.9.1 :) --- extreview/approved.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extreview/approved.rst b/extreview/approved.rst index ea178ceb..96490063 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -24,7 +24,7 @@ Flask-SQLAlchemy :First Approval: 2010-07-25 :Last Review: 2010-07-25 -:Approved Version: 0.9 +:Approved Version: 0.9.1 :Approved License: BSD Notes: Developed by the Flask development head From e3f440d80ffedcc2adb0f058e06a3ee6c56710d9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 23:19:00 +0200 Subject: [PATCH 150/207] Approved Flask-Creole --- extreview/approved.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/extreview/approved.rst b/extreview/approved.rst index 96490063..526a181a 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -30,3 +30,15 @@ Flask-SQLAlchemy Notes: Developed by the Flask development head How to improve: add a better long description to the next release + + +Flask-Creole +------------ + +:First Approval: 2010-07-25 +:Last Review: 2010-07-25 +:Approved Version: 0.4.4 +:Approved License: BSD + +Notes: Flask-Markdown and this should share API, consider that when +approving Flask-Markdown From 54c126e7872b30dc9e5e28303eceea9e827e0231 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 23:25:15 +0200 Subject: [PATCH 151/207] Removed Flask-Creole from the listed file, it's in approved already --- extreview/listed.rst | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/extreview/listed.rst b/extreview/listed.rst index cd658e92..a54c8a77 100644 --- a/extreview/listed.rst +++ b/extreview/listed.rst @@ -28,18 +28,6 @@ package (missing entry in MANIFEST.in) and the test suite does not respond to either "make test" or "python setup.py test". -Flask-Creole ------------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.2 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". Furthermore the README -file is empty. - - flask-csrf ---------- From d455135338d84dfac2a2b98f52cde1d43dd87681 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 23:46:24 +0200 Subject: [PATCH 152/207] Added a workaround for py.test --- tests/flaskext_test.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py index 3614ad58..c7eb37df 100644 --- a/tests/flaskext_test.py +++ b/tests/flaskext_test.py @@ -216,13 +216,22 @@ def test_extension(name, interpreters, flask_dep): # figure out the test command and write a wrapper script. We # can't write that directly into the tox ini because tox does # not invoke the command from the shell so we have no chance - # to pipe the output into a logfile + # to pipe the output into a logfile. The /dev/null hack is + # to trick py.test (if used) into not guessing widths from the + # invoking terminal. test_command = get_test_command(checkout_path) log('Test command: %s', test_command) f = open(checkout_path + '/flaskext-runtest.sh', 'w') - f.write(test_command + ' &> "$1"\n') + f.write(test_command + ' &> "$1" < /dev/null\n') f.close() + # if there is a tox.ini, remove it, it will cause troubles + # for us. Remove it if present, we are running tox ourselves + # afterall. + toxini = os.path.join(checkout_path, 'tox.ini') + if os.path.isfile(toxini): + os.remove(toxini) + create_tox_ini(checkout_path, interpreters, flask_dep) rv = subprocess.call(['tox'], cwd=checkout_path) return TestResult(name, checkout_path, rv, interpreters) From 63a37b75aca15085e685d99f8b7a2e79c83e1f57 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 00:36:09 +0200 Subject: [PATCH 153/207] Improved extension test runner --- tests/flaskext_test.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py index c7eb37df..7a1f179b 100644 --- a/tests/flaskext_test.py +++ b/tests/flaskext_test.py @@ -74,6 +74,9 @@ RESULT_TEMPATE = u'''\ Author License Outcome + {%- for iptr, _ in results[0].logs|dictsort %} + {{ iptr }} + {%- endfor %} @@ -85,6 +88,9 @@ RESULT_TEMPATE = u'''\ {{ result.author }} {{ result.license }} {{ outcome }} + {%- for iptr, _ in result.logs|dictsort %} + see log + {%- endfor %} {%- endfor %} @@ -93,7 +99,8 @@ RESULT_TEMPATE = u'''\

    Detailed test logs for all tests on all platforms: {%- for result in results %} {%- for iptr, log in result.logs|dictsort %} -

    {{ result.name }} - {{ result.version }} [{{ iptr }}]

    +

    + {{ result.name }} - {{ result.version }} [{{ iptr }}]

    {{ log }}
    {%- endfor %} {%- endfor %} @@ -237,14 +244,14 @@ def test_extension(name, interpreters, flask_dep): return TestResult(name, checkout_path, rv, interpreters) -def run_tests(interpreters, only_approved=True): +def run_tests(extensions, interpreters): results = {} create_tdir() log('Packaging Flask') flask_dep = package_flask() log('Running extension tests') log('Temporary Environment: %s', tdir) - for name in iter_extensions(only_approved): + for name in extensions: log('Testing %s', name) result = test_extension(name, interpreters, flask_dep) if result.success: @@ -272,10 +279,21 @@ def main(): help='run against all extensions, not just approved') parser.add_argument('--browse', dest='browse', action='store_true', help='show browser with the result summary') + parser.add_argument('--env', dest='env', default='py25,py26,py27', + help='the tox environments to run against') + parser.add_argument('--extension=', dest='extension', default=None, + help='tests a single extension') args = parser.parse_args() - results = run_tests(['py26'], not args.all) - filename = render_results(results, not args.all) + if args.extension is not None: + only_approved = False + extensions = [args.extension] + else: + only_approved = not args.all + extensions = iter_extensions(only_approved) + + results = run_tests(extensions, [x.strip() for x in args.env.split(',')]) + filename = render_results(results, only_approved) if args.browse: import webbrowser webbrowser.open('file:///' + filename.lstrip('/')) From 140fc45ebd052ea07492c068e45d755d4b42c65c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 00:59:41 +0200 Subject: [PATCH 154/207] Added another workaround. the extension tester is now a pile of hacks --- tests/flaskext_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py index 7a1f179b..84f28b57 100644 --- a/tests/flaskext_test.py +++ b/tests/flaskext_test.py @@ -36,6 +36,10 @@ flaskdir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +# virtualenv hack *cough* +os.environ['PYTHONDONTWRITEBYTECODE'] = '' + + RESULT_TEMPATE = u'''\ Flask-Extension Test Results From 8bd8b014a8393d4b94e6fd192b64e3a2d2f65fce Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 02:30:52 +0200 Subject: [PATCH 155/207] Small fixes in the extension tester --- tests/flaskext_test.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py index 84f28b57..6b669475 100644 --- a/tests/flaskext_test.py +++ b/tests/flaskext_test.py @@ -27,13 +27,16 @@ from setuptools.archive_util import unpack_archive flask_svc_url = 'http://flask.pocoo.org/extensions/' + +# OS X has awful paths when using mkstemp or gettempdir(). I don't +# care about security or clashes here, so pick something that is +# actually rememberable. if sys.platform == 'darwin': _tempdir = '/private/tmp' else: _tempdir = tempfile.gettempdir() tdir = _tempdir + '/flaskext-test' -flaskdir = os.path.abspath(os.path.join(os.path.dirname(__file__), - '..')) +flaskdir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) # virtualenv hack *cough* From 4ab21cd7489e6a7b4e64beda7d8e44fd93557964 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 02:30:58 +0200 Subject: [PATCH 156/207] Approved Flask-Genshi --- extreview/approved.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/extreview/approved.rst b/extreview/approved.rst index 526a181a..a23f5cf8 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -42,3 +42,13 @@ Flask-Creole Notes: Flask-Markdown and this should share API, consider that when approving Flask-Markdown + + +Flask-Genshi +------------ + +:Last-Review: 2010-07-26 +:Reviewed Version: 0.3.1 + +Notes: This is the first template engine extension. When others come +around it would be a good idea to decide on a common interface. From c468ad0bfe8608d28e65a0a2c8ba1093dbd7167c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 02:37:16 +0200 Subject: [PATCH 157/207] Forgot to unlist flask-genshi --- extreview/listed.rst | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/extreview/listed.rst b/extreview/listed.rst index a54c8a77..c5b38b02 100644 --- a/extreview/listed.rst +++ b/extreview/listed.rst @@ -43,19 +43,6 @@ Name is not following Flask extension naming rules. Considered for unlisting. -Flask-Genshi ------------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.3 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". Furthermore the long -description is empty. The zip_safe flag is not set to False which is a -requirement for approved extensions. - - flask-lesscss ------------- From 6aeb6a09aff3115466eb39253ec236ec98348d9e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 02:52:05 +0200 Subject: [PATCH 158/207] Added standard dep on py because some extensions might use py.test and the default available version is on the wrong python path --- tests/flaskext_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py index 6b669475..11cb9fb0 100644 --- a/tests/flaskext_test.py +++ b/tests/flaskext_test.py @@ -200,7 +200,9 @@ tox_template = """[tox] envlist=%(env)s [testenv] -deps=%(deps)s +deps= + %(deps)s + py commands=bash flaskext-runtest.sh {envlogdir}/test.log downloadcache=%(cache)s """ From 89ed7e616e8b1bd16285703e44ecb40c953671cb Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 13:20:46 +0200 Subject: [PATCH 159/207] Approved Flask-Script --- extreview/approved.rst | 18 ++++++++++++++++-- extreview/listed.rst | 14 -------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/extreview/approved.rst b/extreview/approved.rst index a23f5cf8..0e404ac8 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -47,8 +47,22 @@ approving Flask-Markdown Flask-Genshi ------------ -:Last-Review: 2010-07-26 -:Reviewed Version: 0.3.1 +:First Approval: 2010-07-26 +:Last Review: 2010-07-26 +:Approved Version: 0.3.1 +:Approved License: BSD Notes: This is the first template engine extension. When others come around it would be a good idea to decide on a common interface. + + +Flask-Script +------------ + +:First Approval: 2010-07-26 +:Last Review: 2010-07-26 +:Approved Version: 0.3 +:Approved License: BSD + +Notes: Flask-Actions has some overlap. Consider that when approving +Flask-Actions or similar packages. diff --git a/extreview/listed.rst b/extreview/listed.rst index c5b38b02..f39a0e20 100644 --- a/extreview/listed.rst +++ b/extreview/listed.rst @@ -91,20 +91,6 @@ Flask-OpenID Short long description, missing tests. -Flask-Script ------------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.2 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". - -The upcoming 0.3 release looks promising, could need a longer "long -description" in the package index though. - - Flask-Testing ------------- From 27ce5cc0a139d06ed83bd60ace20ad38cdf4ec8f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 16:02:36 +0200 Subject: [PATCH 160/207] Added another rule to the approval list. 2.5-2.7 compatibility --- docs/extensiondev.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index c2a6d2f7..1505e69c 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -316,6 +316,8 @@ extension to be approved you have to follow these guidelines: (``PackageName==dev``). 9. The ``zip_safe`` flag in the setup scrip must be set to ``False``, even if the extension would be safe for zipping. +10. An extension currently has to support Python 2.5, 2.6 as well as + Python 2.7 .. _Flask Extension Wizard: From 3da165f2f56fe7f1b71242157fd4d02d3e37128f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 17:14:17 +0200 Subject: [PATCH 161/207] Fixed typo --- docs/extensiondev.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 1505e69c..44633b33 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -314,7 +314,7 @@ extension to be approved you have to follow these guidelines: link to the documentation, website (if there is one) and there must be a link to automatically install the development version (``PackageName==dev``). -9. The ``zip_safe`` flag in the setup scrip must be set to ``False``, +9. The ``zip_safe`` flag in the setup script must be set to ``False``, even if the extension would be safe for zipping. 10. An extension currently has to support Python 2.5, 2.6 as well as Python 2.7 From 2594602076e223f237a5cf4d0f8742ebc3ad3ab1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 17:14:25 +0200 Subject: [PATCH 162/207] Approved Flask-CouchDB --- extreview/approved.rst | 12 ++++++++++++ extreview/listed.rst | 16 ---------------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/extreview/approved.rst b/extreview/approved.rst index 0e404ac8..59dc2e54 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -66,3 +66,15 @@ Flask-Script Notes: Flask-Actions has some overlap. Consider that when approving Flask-Actions or similar packages. + + +Flask-CouchDB +------------- + +:First Approval: 2010-07-26 +:Last Review: 2010-07-26 +:Approved Version: 0.2.1 +:Approved License: MIT + +There is also Flask-CouchDBKit. Both are fine because they are doing +different things, but the latter is not yet approved. diff --git a/extreview/listed.rst b/extreview/listed.rst index f39a0e20..c4a436c8 100644 --- a/extreview/listed.rst +++ b/extreview/listed.rst @@ -57,22 +57,6 @@ Considered for unlisting, improved version should release as as a testsuite. -flask-mail ----------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.3.1 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". Furthermore the long -description in the package index is a little bit too short. - -Package name should be changed to Flask-Mail with the approval to be -consistent, this might also be the change to improve the API if necessary, -but I don't see any big design problems there. - - Flask-OAuth ----------- From c18f032a822fa6db560c440d7faa7c615826aa30 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 27 Jul 2010 01:29:21 +0200 Subject: [PATCH 163/207] Added a tox-test command that runs Flask tests with tox --- .gitignore | 1 + Makefile | 3 +++ tox.ini | 5 +++++ 3 files changed, 9 insertions(+) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 4844be8e..5c012aaa 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist *.egg *.egg-info _mailinglist +.tox diff --git a/Makefile b/Makefile index 8055e8ef..ab0a8d97 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,9 @@ all: clean-pyc test test: python setup.py test +tox-test: + PYTHONDONTWRITEBYTECODE= tox + ext-test: python tests/flaskext_test.py --browse diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..15911b4c --- /dev/null +++ b/tox.ini @@ -0,0 +1,5 @@ +[tox] +envlist=py25,py26,py27 + +[testenv] +commands=make test From 28f7c1956bc60a81be9a9e5935d72d25b444f3f3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 27 Jul 2010 14:10:42 +0200 Subject: [PATCH 164/207] Approved WTF and Testing --- extreview/approved.rst | 22 ++++++++++++++++++++++ extreview/listed.rst | 33 --------------------------------- 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/extreview/approved.rst b/extreview/approved.rst index 59dc2e54..13db5e66 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -78,3 +78,25 @@ Flask-CouchDB There is also Flask-CouchDBKit. Both are fine because they are doing different things, but the latter is not yet approved. + + +Flask-Testing +------------- + +:First Approval: 2010-07-27 +:Last Review: 2010-07-27 +:Approved Version: 0.2.3 +:Approved License: BSD + +All fine. + + +Flask-WTF +--------- + +:First Approval: 2010-07-27 +:Last Review: 2010-07-27 +:Approved Version: 0.2.3 +:Approved License: BSD + +All fine. diff --git a/extreview/listed.rst b/extreview/listed.rst index c4a436c8..efd8571b 100644 --- a/extreview/listed.rst +++ b/extreview/listed.rst @@ -6,17 +6,6 @@ extension is on the list of extensions on the website. It does not contain extensions that are approved. -Flask-CouchDB -------------- - -:Last-Review: 2010-07-25 -:Reviewed version: 0.2 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". - - Flask-CouchDBKit ---------------- @@ -75,17 +64,6 @@ Flask-OpenID Short long description, missing tests. -Flask-Testing -------------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.2 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". - - Flask-Themes ------------ @@ -108,17 +86,6 @@ package (missing entry in MANIFEST.in) and the test suite does not respond to either "make test" or "python setup.py test". -Flask-WTF ---------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.2.1 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". - - Flask-XML-RPC ------------- From 5cadd9d34da46b909f91a5379d41b90f258d5998 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 27 Jul 2010 14:38:59 +0200 Subject: [PATCH 165/207] Picked codename for 0.6 --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index c1f50998..54a65836 100644 --- a/CHANGES +++ b/CHANGES @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Flask release. Version 0.6 ----------- -Release date to be announced, codename to be decided. +Released on July 27th 2010, codename Whisky - after request functions are now called in reverse order of registration. From 16b101edb6c10a984b0f2960feeeca5a458845a1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 27 Jul 2010 14:40:21 +0200 Subject: [PATCH 166/207] HEAD is 0.7-dev --- CHANGES | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 54a65836..a195fd1f 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,12 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.7 +----------- + +Release date to be announced, codename to be selected + + Version 0.6 ----------- diff --git a/setup.py b/setup.py index b97a33f7..78bd25d0 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ def run_tests(): setup( name='Flask', - version='0.6', + version='0.7', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From 3e297e89d895262e4cf18bfa860f83fbe4b86a61 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 27 Jul 2010 17:15:20 +0200 Subject: [PATCH 167/207] Approved Flask-Themes and Flask-Uploads --- extreview/approved.rst | 22 ++++++++++++++++++++++ extreview/listed.rst | 22 ---------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/extreview/approved.rst b/extreview/approved.rst index 13db5e66..78299933 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -100,3 +100,25 @@ Flask-WTF :Approved License: BSD All fine. + + +Flask-Themes +------------ + +:First Approval: 2010-07-27 +:Last Review: 2010-07-27 +:Approved Version: 0.1.2 +:Approved License: MIT + +All fine. + + +Flask-Uploads +------------- + +:First Approval: 2010-07-27 +:Last Review: 2010-07-27 +:Approved Version: 0.1.2 +:Approved License: MIT + +All fine. diff --git a/extreview/listed.rst b/extreview/listed.rst index efd8571b..7510c8ac 100644 --- a/extreview/listed.rst +++ b/extreview/listed.rst @@ -64,28 +64,6 @@ Flask-OpenID Short long description, missing tests. -Flask-Themes ------------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.1 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". - - -Flask-Uploads -------------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.1 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". - - Flask-XML-RPC ------------- From 750c3a07d8a82bde3cf8d60c7ef29e26ef149a1c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 27 Jul 2010 18:46:29 +0200 Subject: [PATCH 168/207] Added artwork to MANIFEST.in --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 166311d4..3fef8b5b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include Makefile CHANGES LICENSE AUTHORS +recursive-include artwork * recursive-include tests * recursive-include examples * recursive-include docs * From 3561a398b1a9ad4cfe7f056fe10811930f54b502 Mon Sep 17 00:00:00 2001 From: Aku Kotkavuo Date: Wed, 28 Jul 2010 05:27:10 +0800 Subject: [PATCH 169/207] Made one sentence easier to parse in docs/foreword --- docs/foreword.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/foreword.rst b/docs/foreword.rst index 3a7521d4..308de9c5 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -40,8 +40,8 @@ was implemented in Flask itself. There are currently extensions for object relational mappers, form validation, upload handling, various open authentication technologies and more. -However Flask is not much code and built in a very solid foundation and -with that very easy to adapt for large applications. If you are +However Flask is not much code and it is built on a very solid foundation +and with that it is very easy to adapt for large applications. If you are interested in that, check out the :ref:`becomingbig` chapter. If you are curious about the Flask design principles, head over to the From 4cd5201cdd03e14edbceb24620717bbe82300524 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 26 Jul 2010 14:45:05 +0800 Subject: [PATCH 170/207] use custom tox file named tox-flask-test.ini, dont delete the real tox.ini --- tests/flaskext_test.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py index 11cb9fb0..9dc9ad67 100644 --- a/tests/flaskext_test.py +++ b/tests/flaskext_test.py @@ -209,7 +209,7 @@ downloadcache=%(cache)s def create_tox_ini(checkout_path, interpreters, flask_dep): - tox_path = os.path.join(checkout_path, 'tox.ini') + tox_path = os.path.join(checkout_path, 'tox-flask-test.ini') if not os.path.exists(tox_path): with open(tox_path, 'w') as f: f.write(tox_template % { @@ -217,6 +217,7 @@ def create_tox_ini(checkout_path, interpreters, flask_dep): 'cache': tdir, 'deps': flask_dep }) + return tox_path def iter_extensions(only_approved=True): @@ -244,12 +245,9 @@ def test_extension(name, interpreters, flask_dep): # if there is a tox.ini, remove it, it will cause troubles # for us. Remove it if present, we are running tox ourselves # afterall. - toxini = os.path.join(checkout_path, 'tox.ini') - if os.path.isfile(toxini): - os.remove(toxini) create_tox_ini(checkout_path, interpreters, flask_dep) - rv = subprocess.call(['tox'], cwd=checkout_path) + rv = subprocess.call(['tox', '-c', 'tox-flask-test.ini'], cwd=checkout_path) return TestResult(name, checkout_path, rv, interpreters) From 0906f7d58930e047f7ec67290997b552e587e219 Mon Sep 17 00:00:00 2001 From: florentx Date: Wed, 28 Jul 2010 00:19:47 +0800 Subject: [PATCH 171/207] Typos. --- docs/_themes | 1 - docs/styleguide.rst | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) delete mode 160000 docs/_themes diff --git a/docs/_themes b/docs/_themes deleted file mode 160000 index 3d964b66..00000000 --- a/docs/_themes +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3d964b660442e23faedf801caed6e3c7bd42d5c9 diff --git a/docs/styleguide.rst b/docs/styleguide.rst index 1387d4a6..ec699052 100644 --- a/docs/styleguide.rst +++ b/docs/styleguide.rst @@ -31,7 +31,7 @@ Continuing long statements: .order_by(MyModel.name.desc()) \ .limit(10) - If you break in a statement with parentheses or brances, align to the + If you break in a statement with parentheses or braces, align to the braces:: this_is_a_very_long(function_call, 'with many parameters', @@ -105,8 +105,8 @@ Yoda statements are a nogo: pass Comparisons: - - against arbitary types: ``==`` and ``!=`` - - against singletones with ``is`` and ``is not`` (eg: ``foo is not + - against arbitrary types: ``==`` and ``!=`` + - against singletons with ``is`` and ``is not`` (eg: ``foo is not None``) - never compare something with `True` or `False` (for example never do ``foo == False``, do ``not foo`` instead) @@ -125,7 +125,7 @@ Naming Conventions - Class names: ``CamelCase``, with acronyms kept uppercase (``HTTPWriter`` and not ``HttpWriter``) - Variable names: ``lowercase_with_underscores`` -- Method and functin names: ``lowercase_with_underscores`` +- Method and function names: ``lowercase_with_underscores`` - Constants: ``UPPERCASE_WITH_UNDERSCORES`` - precompiled regular expressions: ``name_re`` @@ -151,9 +151,9 @@ Docstrings Docstring conventions: All docstrings are formatted with reStructuredText as understood by Sphinx. Depending on the number of lines in the docstring, they are - layed out differently. If it's just one line, the closing tripple + layed out differently. If it's just one line, the closing triple quote is on the same line as the opening, otherwise the text is on - the same line as the opening quote and the tripple quote that closes + the same line as the opening quote and the triple quote that closes the string on its own line:: def foo(): From 8a14a875d26dc0923c4c52ed1e56eb112d81e723 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 28 Jul 2010 01:26:15 +0200 Subject: [PATCH 172/207] Added the upcoming bugfix release to the mainbranches CHANGES --- CHANGES | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES b/CHANGES index a195fd1f..0bd84823 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,10 @@ Version 0.7 Release date to be announced, codename to be selected +Version 0.6.1 +------------- + +Bugfix release, release date to be announced. Version 0.6 ----------- From dbf55de7e8bfcf7cde967aa72ea006b764d484ac Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 28 Jul 2010 01:25:08 +0200 Subject: [PATCH 173/207] Fixed an issue where the default `OPTIONS` response was not exposing all valid methods in the `Allow` header. This fixes #97 Signed-off-by: Armin Ronacher --- CHANGES | 3 +++ flask/app.py | 23 +++++++++++++++++++---- tests/flask_tests.py | 11 +++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 0bd84823..b9aa3739 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,9 @@ Version 0.6.1 Bugfix release, release date to be announced. +- Fixed an issue where the default `OPTIONS` response was + not exposing all valid methods in the `Allow` header. + Version 0.6 ----------- diff --git a/flask/app.py b/flask/app.py index 9b6b7836..72ed6292 100644 --- a/flask/app.py +++ b/flask/app.py @@ -19,7 +19,8 @@ from jinja2 import Environment from werkzeug import ImmutableDict from werkzeug.routing import Map, Rule -from werkzeug.exceptions import HTTPException, InternalServerError +from werkzeug.exceptions import HTTPException, InternalServerError, \ + MethodNotAllowed from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ _tojson_filter, _endpoint_from_view_func @@ -689,14 +690,28 @@ class Flask(_PackageBoundObject): # if we provide automatic options for this URL and the # request came with the OPTIONS method, reply automatically if rule.provide_automatic_options and req.method == 'OPTIONS': - rv = self.response_class() - rv.allow.update(rule.methods) - return rv + return self._make_default_options_response() # otherwise dispatch to the handler for that endpoint return self.view_functions[rule.endpoint](**req.view_args) except HTTPException, e: return self.handle_http_exception(e) + def _make_default_options_response(self): + # This would be nicer in Werkzeug 0.7, which however currently + # is not released. Werkzeug 0.7 provides a method called + # allowed_methods() that returns all methods that are valid for + # a given path. + methods = [] + try: + _request_ctx_stack.top.url_adapter.match(method='--') + except MethodNotAllowed, e: + methods = e.valid_methods + except HTTPException, e: + pass + rv = self.response_class() + rv.allow.update(methods) + return rv + def make_response(self, rv): """Converts the return value from a view function to a real response object that is an instance of :attr:`response_class`. diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 3000e41c..93bdfef7 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -120,6 +120,17 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] assert rv.data == '' + def test_options_on_multiple_rules(self): + app = flask.Flask(__name__) + @app.route('/', methods=['GET', 'POST']) + def index(): + return 'Hello World' + @app.route('/', methods=['PUT']) + def index_put(): + return 'Aha!' + rv = app.test_client().open('/', method='OPTIONS') + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] + def test_request_dispatching(self): app = flask.Flask(__name__) @app.route('/') From 738c66eff30d1a5f5672c22a32a02c29d9c38a2b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 28 Jul 2010 01:32:39 +0200 Subject: [PATCH 174/207] Fixed submodules. Once again -.- --- docs/_themes | 1 + 1 file changed, 1 insertion(+) create mode 160000 docs/_themes diff --git a/docs/_themes b/docs/_themes new file mode 160000 index 00000000..3d964b66 --- /dev/null +++ b/docs/_themes @@ -0,0 +1 @@ +Subproject commit 3d964b660442e23faedf801caed6e3c7bd42d5c9 From 43ae651c3091a9c1252726310abe5c9116ee2e53 Mon Sep 17 00:00:00 2001 From: Aku Kotkavuo Date: Wed, 28 Jul 2010 07:10:31 +0800 Subject: [PATCH 175/207] Proofread docs/quickstart --- docs/quickstart.rst | 161 ++++++++++++++++++++++---------------------- 1 file changed, 79 insertions(+), 82 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index d7cf5982..a85ed443 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -3,7 +3,7 @@ Quickstart ========== -Eager to get started? This page gives a good introduction in how to gets +Eager to get started? This page gives a good introduction in how to get started with Flask. This assumes you already have Flask installed. If you do not, head over to the :ref:`installation` section. @@ -37,14 +37,14 @@ see your hello world greeting. So what did that code do? -1. first we imported the :class:`~flask.Flask` class. An instance of this +1. First we imported the :class:`~flask.Flask` class. An instance of this class will be our WSGI application. The first argument is the name of the application's module. If you are using a single module (like here) you should use `__name__` because depending on if it's started as application or imported as module the name will be different (``'__main__'`` versus the actual import name). For more information on that, have a look at the :class:`~flask.Flask` documentation. -2. next we create an instance of it. We pass it the name of the module / +2. Next we create an instance of it. We pass it the name of the module / package. This is needed so that Flask knows where it should look for templates, static files and so on. 3. Then we use the :meth:`~flask.Flask.route` decorator to tell Flask @@ -81,7 +81,7 @@ To stop the server, hit control-C. Debug Mode ---------- -Now that :meth:`~flask.Flask.run` method is nice to start a local +The :meth:`~flask.Flask.run` method is nice to start a local development server, but you would have to restart it manually after each change you do to code. That is not very nice and Flask can do better. If you enable the debug support the server will reload itself on code changes @@ -101,11 +101,10 @@ Both will have exactly the same effect. .. admonition:: Attention - The interactive debugger however does not work in forking environments - which makes it nearly impossible to use on production servers but the - debugger still allows the execution of arbitrary code which makes it a - major security risk and **must never be used on production machines** - because of that. + Even though the interactive debugger does not work in forking environments + (which makes it nearly impossible to use on production servers), it still + allows the execution of arbitrary code. That makes it a major security + risk and therefore it **must never be used on production machines**. Screenshot of the debugger in action: @@ -118,11 +117,14 @@ Screenshot of the debugger in action: Routing ------- -As you have seen above, the :meth:`~flask.Flask.route` decorator is used -to bind a function to a URL. But there is more to it! You can make -certain parts of the URL dynamic and attach multiple rules to a function. +Modern web applications have beautiful URLs. This helps people remember +the URLs which is especially handy for applications that are used from +mobile devices with slower network connections. If the user can directly +go to the desired page without having to hit the index page it is more +likely he will like the page and come back next time. -Here some examples:: +As you have seen above, the :meth:`~flask.Flask.route` decorator is used +to bind a function to a URL. Here are some basic examples:: @app.route('/') def index(): @@ -132,20 +134,16 @@ Here some examples:: def hello(): return 'Hello World' +But there is more to it! You can make certain parts of the URL dynamic +and attach multiple rules to a function. Variable Rules `````````````` -Modern web applications have beautiful URLs. This helps people remember -the URLs which is especially handy for applications that are used from -mobile devices with slower network connections. If the user can directly -go to the desired page without having to hit the index page it is more -likely he will like the page and come back next time. - To add variable parts to a URL you can mark these special sections as ````. Such a part is then passed as keyword argument to your function. Optionally a converter can be specified by specifying a -rule with ````. Here some nice examples:: +rule with ````. Here are some nice examples:: @app.route('/user/') def show_user_profile(username): @@ -203,12 +201,12 @@ The following converters exist: URL Building ```````````` -If it can match URLs, can it also generate them? Of course you can. To +If it can match URLs, can it also generate them? Of course it can. To build a URL to a specific function you can use the :func:`~flask.url_for` function. It accepts the name of the function as first argument and a number of keyword arguments, each corresponding to the variable part of the URL rule. Unknown variable parts are appended to the URL as query -parameter. Here some examples: +parameter. Here are some examples: >>> from flask import Flask, url_for >>> app = Flask(__name__) @@ -256,7 +254,7 @@ HTTP Methods HTTP (the protocol web applications are speaking) knows different methods to access URLs. By default a route only answers to `GET` requests, but that can be changed by providing the `methods` argument to the -:meth:`~flask.Flask.route` decorator. Here some examples:: +:meth:`~flask.Flask.route` decorator. Here are some examples:: @app.route('/login', methods=['GET', 'POST']) def login(): @@ -272,25 +270,24 @@ protocol) demands, so you can completely ignore that part of the HTTP specification. Likewise as of Flask 0.6, `OPTIONS` is implemented for you as well automatically. -You have no idea what an HTTP method is? Worry not, here quick -introduction in HTTP methods and why they matter: +You have no idea what an HTTP method is? Worry not, here is a quick +introduction to HTTP methods and why they matter: The HTTP method (also often called "the verb") tells the server what the clients wants to *do* with the requested page. The following methods are very common: `GET` - The Browser tells the server: just *get* me the information stored on - that page and send them to me. This is probably the most common - method. + The browser tells the server to just *get* the information stored on + that page and send it. This is probably the most common method. `HEAD` - The Browser tells the server: get me the information, but I am only + The browser tells the server to get the information, but it is only interested in the *headers*, not the content of the page. An application is supposed to handle that as if a `GET` request was - received but not deliver the actual contents. In Flask you don't have - to deal with that at all, the underlying Werkzeug library handles that - for you. + received but to not deliver the actual content. In Flask you don't + have to deal with that at all, the underlying Werkzeug library handles + that for you. `POST` The browser tells the server that it wants to *post* some new @@ -301,27 +298,27 @@ very common: `PUT` Similar to `POST` but the server might trigger the store procedure multiple times by overwriting the old values more than once. Now you - might be asking why this is any useful, but there are some good - reasons to do that. Consider the connection is lost during - transmission, in that situation a system between the browser and the - server might sent the request safely a second time without breaking + might be asking why is this useful, but there are some good reasons + to do it this way. Consider that the connection gets lost during + transmission: in this situation a system between the browser and the + server might receive the request safely a second time without breaking things. With `POST` that would not be possible because it must only be triggered once. `DELETE` - Remove the information that the given location. + Remove the information at the given location. `OPTIONS` - Provides a quick way for a requesting client to figure out which - methods are supported by this URL. Starting with Flask 0.6, this - is implemented for you automatically. + Provides a quick way for a client to figure out which methods are + supported by this URL. Starting with Flask 0.6, this is implemented + for you automatically. Now the interesting part is that in HTML4 and XHTML1, the only methods a -form might submit to the server are `GET` and `POST`. But with JavaScript -and future HTML standards you can use other methods as well. Furthermore -HTTP became quite popular lately and there are more things than browsers -that are speaking HTTP. (Your revision control system for instance might -speak HTTP) +form can submit to the server are `GET` and `POST`. But with JavaScript +and future HTML standards you can use the other methods as well. Furthermore +HTTP has become quite popular lately and browsers are no longer the only +clients that are using HTTP. For instance, many revision control system +use it. .. _HTTP RFC: http://www.ietf.org/rfc/rfc2068.txt @@ -383,7 +380,7 @@ to the :ref:`templating` section of the documentation or the official `Jinja2 Template Documentation `_ for more information. -Here an example template: +Here is an example template: .. sourcecode:: html+jinja @@ -411,7 +408,7 @@ markup to HTML) you can mark it as safe by using the :class:`~jinja2.Markup` class or by using the ``|safe`` filter in the template. Head over to the Jinja 2 documentation for more examples. -Here a basic introduction in how the :class:`~jinja2.Markup` class works: +Here is a basic introduction to how the :class:`~jinja2.Markup` class works: >>> from flask import Markup >>> Markup('Hello %s!') % 'hacker' @@ -425,12 +422,12 @@ u'Marked up \xbb HTML' Autoescaping is no longer enabled for all templates. The following extensions for templates trigger autoescaping: ``.html``, ``.htm``, - ``.xml``, ``.xhtml``. Templates loaded from string will have + ``.xml``, ``.xhtml``. Templates loaded from a string will have autoescaping disabled. -.. [#] Unsure what that :class:`~flask.g` object is? It's something you - can store information on yourself, check the documentation of that - object (:class:`~flask.g`) and the :ref:`sqlite3` for more +.. [#] Unsure what that :class:`~flask.g` object is? It's something in which + you can store information for your own needs, check the documentation of + that object (:class:`~flask.g`) and the :ref:`sqlite3` for more information. @@ -454,10 +451,9 @@ Context Locals If you want to understand how that works and how you can implement tests with context locals, read this section, otherwise just skip it. -Certain objects in Flask are global objects, but not just a standard -global object, but actually a proxy to an object that is local to a -specific context. What a mouthful. But that is actually quite easy to -understand. +Certain objects in Flask are global objects, but not of the usual kind. +These objects are actually proxies to objects that are local to a specific +context. What a mouthful. But that is actually quite easy to understand. Imagine the context being the handling thread. A request comes in and the webserver decides to spawn a new thread (or something else, the @@ -469,13 +465,13 @@ It does that in an intelligent way that one application can invoke another application without breaking. So what does this mean to you? Basically you can completely ignore that -this is the case unless you are unittesting or something different. You +this is the case unless you are doing something like unittesting. You will notice that code that depends on a request object will suddenly break because there is no request object. The solution is creating a request object yourself and binding it to the context. The easiest solution for unittesting is by using the :meth:`~flask.Flask.test_request_context` context manager. In combination with the `with` statement it will bind a -test request so that you can interact with it. Here an example:: +test request so that you can interact with it. Here is an example:: from flask import request @@ -497,8 +493,8 @@ The Request Object `````````````````` The request object is documented in the API section and we will not cover -it here in detail (see :class:`~flask.request`), but just mention some of -the most common operations. First of all you have to import it from the +it here in detail (see :class:`~flask.request`). Here is a broad overview of +some of the most common operations. First of all you have to import it from the `flask` module:: from flask import request @@ -506,7 +502,7 @@ the `flask` module:: The current request method is available by using the :attr:`~flask.request.method` attribute. To access form data (data transmitted in a `POST` or `PUT` request) you can use the -:attr:`~flask.request.form` attribute. Here a full example of the two +:attr:`~flask.request.form` attribute. Here is a full example of the two attributes mentioned above:: @app.route('/login', methods=['POST', 'GET']) @@ -534,19 +530,18 @@ To access parameters submitted in the URL (``?key=value``) you can use the We recommend accessing URL parameters with `get` or by catching the `KeyError` because users might change the URL and presenting them a 400 -bad request page in that case is a bit user unfriendly. +bad request page in that case is not user friendly. -For a full list of methods and attributes on that object, head over to the -:class:`~flask.request` documentation. +For a full list of methods and attributes of the request object, head over +to the :class:`~flask.request` documentation. File Uploads ```````````` -Obviously you can handle uploaded files with Flask just as easy. Just -make sure not to forget to set the ``enctype="multipart/form-data"`` -attribute on your HTML form, otherwise the browser will not transmit your -files at all. +You can handle uploaded files with Flask easily. Just make sure not to +forget to set the ``enctype="multipart/form-data"`` attribute on your HTML +form, otherwise the browser will not transmit your files at all. Uploaded files are stored in memory or at a temporary location on the filesystem. You can access those files by looking at the @@ -554,8 +549,8 @@ filesystem. You can access those files by looking at the uploaded file is stored in that dictionary. It behaves just like a standard Python :class:`file` object, but it also has a :meth:`~werkzeug.FileStorage.save` method that allows you to store that -file on the filesystem of the server. Here a simple example how that -works:: +file on the filesystem of the server. Here is a simple example showing how +that works:: from flask import request @@ -600,8 +595,8 @@ Redirects and Errors -------------------- To redirect a user to somewhere else you can use the -:func:`~flask.redirect` function, to abort a request early with an error -code the :func:`~flask.abort` function. Here an example how this works:: +:func:`~flask.redirect` function. To abort a request early with an error +code use the :func:`~flask.abort` function. Here an example how this works:: from flask import abort, redirect, url_for @@ -681,7 +676,7 @@ sessions work:: The here mentioned :func:`~flask.escape` does escaping for you if you are not using the template engine (like in this example). -.. admonition:: How to generate good Secret Keys +.. admonition:: How to generate good secret keys The problem with random is that it's hard to judge what random is. And a secret key should be as random as possible. Your operating system @@ -715,16 +710,17 @@ Logging .. versionadded:: 0.3 -Sometimes you might be in the situation where you deal with data that -should be correct, but actually is not. For example you have some client -side code that sends an HTTP request to the server, and it's obviously +Sometimes you might be in a situation where you deal with data that +should be correct, but actually is not. For example you may have some client +side code that sends an HTTP request to the server but it's obviously malformed. This might be caused by a user tempering with the data, or the -client code failed. Most the time, it's okay to reply with ``400 Bad -Request`` in that situation, but other times it is not and the code has to -continue working. +client code failing. Most of the time, it's okay to reply with ``400 Bad +Request`` in that situation, but sometimes that won't do and the code has +to continue working. -Yet you want to log that something fishy happened. This is where loggers -come in handy. As of Flask 0.3 a logger is preconfigured for you to use. +You may still want to log that something fishy happened. This is where +loggers come in handy. As of Flask 0.3 a logger is preconfigured for you +to use. Here are some example log calls:: @@ -733,8 +729,9 @@ Here are some example log calls:: app.logger.error('An error occurred') The attached :attr:`~flask.Flask.logger` is a standard logging -:class:`~logging.Logger`, so head over to the official stdlib -documentation for more information. +:class:`~logging.Logger`, so head over to the official `logging +documentation `_ for more +information. Hooking in WSGI Middlewares --------------------------- From 952967fcab0ffccc6b67485e8eed994a69d38d03 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 28 Jul 2010 01:39:25 +0200 Subject: [PATCH 176/207] In 0.7, make_default_options_response is a public API --- CHANGES | 4 ++++ flask/app.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index b9aa3739..03611cc8 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,10 @@ Version 0.7 Release date to be announced, codename to be selected +- Added :meth:`~flask.Flask.make_default_options_response` + which can be used by subclasses to alter the default + behaviour for `OPTIONS` responses. + Version 0.6.1 ------------- diff --git a/flask/app.py b/flask/app.py index 72ed6292..faa5b168 100644 --- a/flask/app.py +++ b/flask/app.py @@ -690,13 +690,19 @@ class Flask(_PackageBoundObject): # if we provide automatic options for this URL and the # request came with the OPTIONS method, reply automatically if rule.provide_automatic_options and req.method == 'OPTIONS': - return self._make_default_options_response() + return self.make_default_options_response() # otherwise dispatch to the handler for that endpoint return self.view_functions[rule.endpoint](**req.view_args) except HTTPException, e: return self.handle_http_exception(e) - def _make_default_options_response(self): + def make_default_options_response(self): + """This method is called to create the default `OPTIONS` response. + This can be changed through subclassing to change the default + behaviour of `OPTIONS` responses. + + .. versionadded:: 0.7 + """ # This would be nicer in Werkzeug 0.7, which however currently # is not released. Werkzeug 0.7 provides a method called # allowed_methods() that returns all methods that are valid for From ed1b34c7249cc79c3287e5becbdfeca6d2155014 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 29 Jul 2010 11:03:56 +0200 Subject: [PATCH 177/207] Approved Flask-Mail --- extreview/approved.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/extreview/approved.rst b/extreview/approved.rst index 78299933..5e8699f5 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -122,3 +122,14 @@ Flask-Uploads :Approved License: MIT All fine. + + +Flask-Mail +---------- + +:First Approval: 2010-07-29 +:Last Review: 2010-07-29 +:Approved Version: 0.3.4 +:Approved License: BSD + +All fine. From 801918603cb075f96ff6c08a6fc6fb165f1eecda Mon Sep 17 00:00:00 2001 From: Stephane Wirtel Date: Thu, 29 Jul 2010 18:51:54 +0800 Subject: [PATCH 178/207] Remove an unused function --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index a2d951b4..53cee0d7 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -33,7 +33,7 @@ except ImportError: json_available = False -from werkzeug import Headers, wrap_file, is_resource_modified, cached_property +from werkzeug import Headers, wrap_file, cached_property from werkzeug.exceptions import NotFound from jinja2 import FileSystemLoader From 778e44e39eb42ff6cbee376ce40d1a52a25cb0ce Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 30 Jul 2010 00:03:06 +0200 Subject: [PATCH 179/207] Improved error message for configuration files --- flask/config.py | 6 +++++- tests/flask_tests.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/flask/config.py b/flask/config.py index cf9ad541..a27b362d 100644 --- a/flask/config.py +++ b/flask/config.py @@ -116,7 +116,11 @@ class Config(dict): filename = os.path.join(self.root_path, filename) d = type(sys)('config') d.__file__ = filename - execfile(filename, d.__dict__) + try: + execfile(filename, d.__dict__) + except IOError, e: + e.strerror = 'Unable to load configuration file (%s)' % e.strerror + raise self.from_object(d) def from_object(self, obj): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 93bdfef7..010bc42e 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1018,6 +1018,18 @@ class ConfigTestCase(unittest.TestCase): finally: os.environ = env + def test_config_missing(self): + app = flask.Flask(__name__) + try: + app.config.from_pyfile('missing.cfg') + except IOError, e: + msg = str(e) + assert msg.startswith('[Errno 2] Unable to load configuration ' + 'file (No such file or directory):') + assert msg.endswith("missing.cfg'") + else: + assert 0, 'expected config' + class SubdomainTestCase(unittest.TestCase): From d09aa3765087bf6e8b8323008e4c1f160db2cd3f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 30 Jul 2010 10:51:14 +0200 Subject: [PATCH 180/207] Approved Flask-XML-RPC --- extreview/approved.rst | 11 +++++++++++ extreview/listed.rst | 9 --------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/extreview/approved.rst b/extreview/approved.rst index 5e8699f5..58d18e72 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -133,3 +133,14 @@ Flask-Mail :Approved License: BSD All fine. + + +Flask-XML-RPC +------------- + +:First Approval: 2010-07-30 +:Last Review: 2010-07-30 +:Approved Version: 0.1.2 +:Approved License: MIT + +All fine. diff --git a/extreview/listed.rst b/extreview/listed.rst index 7510c8ac..474213cb 100644 --- a/extreview/listed.rst +++ b/extreview/listed.rst @@ -62,12 +62,3 @@ Flask-OpenID :Reviewed Version: 1.0.1 Short long description, missing tests. - - -Flask-XML-RPC -------------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.2.1 - -Missing tests, API wise it would be fine for approval. From ff2786d8afd6eba92ad75a22c2ad9275bd970f57 Mon Sep 17 00:00:00 2001 From: jgraeme Date: Tue, 3 Aug 2010 00:07:10 +0800 Subject: [PATCH 181/207] Fix some typos in the docs --- docs/extensiondev.rst | 8 ++++---- docs/patterns/errorpages.rst | 2 +- docs/patterns/jquery.rst | 8 ++++---- docs/patterns/lazyloading.rst | 2 +- docs/patterns/wtforms.rst | 4 ++-- docs/quickstart.rst | 2 +- docs/security.rst | 4 ++-- docs/upgrading.rst | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 44633b33..cfad85f0 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -67,7 +67,7 @@ First we create the following folder structure:: setup.py LICENSE -Here the contents of the most important files: +Here's the contents of the most important files: flaskext/__init__.py ```````````````````` @@ -171,7 +171,7 @@ controller object that can be used to connect to the database. The Extension Code ------------------ -Here the contents of the `flaskext/sqlite3.py` for copy/paste:: +Here's the contents of the `flaskext/sqlite3.py` for copy/paste:: from __future__ import absolute_import import sqlite3 @@ -196,7 +196,7 @@ Here the contents of the `flaskext/sqlite3.py` for copy/paste:: g.sqlite3_db.close() return response -So here what the lines of code do: +So here's what the lines of code do: 1. the ``__future__`` import is necessary to activate absolute imports. This is needed because otherwise we could not call our module @@ -237,7 +237,7 @@ If you don't need that, you can go with initialization functions. Initialization Functions ------------------------ -Here how the module would look like with initialization functions:: +Here's what the module would look like with initialization functions:: from __future__ import absolute_import import sqlite3 diff --git a/docs/patterns/errorpages.rst b/docs/patterns/errorpages.rst index 5b4f6e80..95677644 100644 --- a/docs/patterns/errorpages.rst +++ b/docs/patterns/errorpages.rst @@ -5,7 +5,7 @@ Flask comes with a handy :func:`~flask.abort` function that aborts a request with an HTTP error code early. It will also provide a plain black and white error page for you with a basic description, but nothing fancy. -Depening on the error code it is less or more likely for the user to +Depending on the error code it is less or more likely for the user to actually see such an error. Common Error Codes diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index f2ca39c5..46285864 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -53,8 +53,8 @@ is quite simple: it's on localhost port something and directly on the root of that server. But what if you later decide to move your application to a different location? For example to ``http://example.com/myapp``? On the server side this never was a problem because we were using the handy -:func:`~flask.url_for` function that did could answer that question for -us, but if we are using jQuery we should better not hardcode the path to +:func:`~flask.url_for` function that could answer that question for +us, but if we are using jQuery we should not hardcode the path to the application but make that dynamic, so how can we do that? A simple method would be to add a script tag to our page that sets a @@ -118,9 +118,9 @@ special error reporting in that case. The HTML -------- -You index.html template either has to extend a `layout.html` template with +Your index.html template either has to extend a `layout.html` template with jQuery loaded and the `$SCRIPT_ROOT` variable set, or do that on the top. -Here the HTML code needed for our little application (`index.html`). +Here's the HTML code needed for our little application (`index.html`). Notice that we also drop the script directly into the HTML here. It is usually a better idea to have that in a separate script file: diff --git a/docs/patterns/lazyloading.rst b/docs/patterns/lazyloading.rst index b1fe4cbf..03b293d8 100644 --- a/docs/patterns/lazyloading.rst +++ b/docs/patterns/lazyloading.rst @@ -74,7 +74,7 @@ function but internally imports the real function on first use:: return self.view(*args, **kwargs) What's important here is is that `__module__` and `__name__` are properly -set. This is used by Flask internally to figure out how to do name the +set. This is used by Flask internally to figure out how to name the URL rules in case you don't provide a name for the rule yourself. Then you can define your central place to combine the views like this:: diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index 8a113cc5..93824df7 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -77,7 +77,7 @@ how easy this is. WTForms does half the form generation for us already. To make it even nicer, we can write a macro that renders a field with label and a list of errors if there are any. -Here an example `_formhelpers.html` template with such a macro: +Here's an example `_formhelpers.html` template with such a macro: .. sourcecode:: html+jinja @@ -93,7 +93,7 @@ Here an example `_formhelpers.html` template with such a macro: {% endmacro %} This macro accepts a couple of keyword arguments that are forwarded to -WTForm's field function that renders the field for us. They keyword +WTForm's field function that renders the field for us. The keyword arguments will be inserted as HTML attributes. So for example you can call ``render_field(form.username, class='username')`` to add a class to the input element. Note that WTForms returns standard Python unicode diff --git a/docs/quickstart.rst b/docs/quickstart.rst index a85ed443..c115fa70 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -231,7 +231,7 @@ parameter. Here are some examples: /user/John%20Doe (This also uses the :meth:`~flask.Flask.test_request_context` method -explained below. It basically tells flask to think we are handling a +explained below. It basically tells Flask to think we are handling a request even though we are not, we are in an interactive Python shell. Have a look at the explanation below. :ref:`context-locals`). diff --git a/docs/security.rst b/docs/security.rst index 45fff0ca..f3193d62 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -72,7 +72,7 @@ do stupid things without them knowing. Say you have a specific URL that, when you sent `POST` requests to will delete a user's profile (say `http://example.com/user/delete`). If an -attacker now creates a page that sents a post request to that page with +attacker now creates a page that sends a post request to that page with some JavaScript he just has to trick some users to that page and their profiles will end up being deleted. @@ -163,6 +163,6 @@ page loaded the data from the JSON response is in the `captured` array. Because it is a syntax error in JavaScript to have an object literal (``{...}``) toplevel an attacker could not just do a request to an external URL with the script tag to load up the data. So what Flask does -is only allowing objects as toplevel elements when using +is to only allow objects as toplevel elements when using :func:`~flask.jsonify`. Make sure to do the same when using an ordinary JSON generate function. diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 747fdb72..24966e7e 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -2,7 +2,7 @@ Upgrading to Newer Releases =========================== Flask itself is changing like any software is changing over time. Most of -the changes are the nice kind, the kind where you don't have th change +the changes are the nice kind, the kind where you don't have to change anything in your code to profit from a new release. However every once in a while there are changes that do require some From 549af6229039e7fed7bcb2079737e45c27af998e Mon Sep 17 00:00:00 2001 From: jgraeme Date: Tue, 3 Aug 2010 00:18:19 +0800 Subject: [PATCH 182/207] Fix some typos in the docstrings --- flask/app.py | 4 ++-- flask/helpers.py | 2 +- flask/module.py | 4 ++-- flask/wrappers.py | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/flask/app.py b/flask/app.py index faa5b168..f32e4dd8 100644 --- a/flask/app.py +++ b/flask/app.py @@ -215,7 +215,7 @@ class Flask(_PackageBoundObject): #: A dictionary of all view functions registered. The keys will #: be function names which are also used to generate URLs and #: the values are the function objects themselves. - #: to register a view function, use the :meth:`route` decorator. + #: To register a view function, use the :meth:`route` decorator. self.view_functions = {} #: A dictionary of all registered error handlers. The key is @@ -242,7 +242,7 @@ class Flask(_PackageBoundObject): self.after_request_funcs = {} #: A dictionary with list of functions that are called without argument - #: to populate the template context. They key of the dictionary is the + #: to populate the template context. The key of the dictionary is the #: name of the module this function is active for, `None` for all #: requests. Each returns a dictionary that the template context is #: updated with. To register a function here, use the diff --git a/flask/helpers.py b/flask/helpers.py index 53cee0d7..a6c18a33 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -146,7 +146,7 @@ def url_for(endpoint, **values): """Generates a URL to the given endpoint with the method provided. The endpoint is relative to the active module if modules are in use. - Here some examples: + Here are some examples: ==================== ======================= ============================= Active Module Target Endpoint Target Function diff --git a/flask/module.py b/flask/module.py index e6e1ee36..29350eb6 100644 --- a/flask/module.py +++ b/flask/module.py @@ -54,7 +54,7 @@ class Module(_PackageBoundObject): to be provided to keep them apart. If different import names are used, the rightmost part of the import name is used as name. - Here an example structure for a larger appliation:: + Here's an example structure for a larger application:: /myapplication /__init__.py @@ -73,7 +73,7 @@ class Module(_PackageBoundObject): app.register_module(admin, url_prefix='/admin') app.register_module(frontend) - And here an example view module (`myapplication/views/admin.py`):: + And here's an example view module (`myapplication/views/admin.py`):: from flask import Module diff --git a/flask/wrappers.py b/flask/wrappers.py index c578170c..4db1e782 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -17,7 +17,7 @@ from .globals import _request_ctx_stack class Request(RequestBase): - """The request object used by default in flask. Remembers the + """The request object used by default in Flask. Remembers the matched endpoint and view arguments. It is what ends up as :class:`~flask.request`. If you want to replace @@ -77,8 +77,8 @@ class Request(RequestBase): class Response(ResponseBase): - """The response object that is used by default in flask. Works like the - response object from Werkzeug but is set to have a HTML mimetype by + """The response object that is used by default in Flask. Works like the + response object from Werkzeug but is set to have an HTML mimetype by default. Quite often you don't have to create this object yourself because :meth:`~flask.Flask.make_response` will take care of that for you. From f52f4fd31b9858f8b42be3fbb2818440e58978b4 Mon Sep 17 00:00:00 2001 From: Priit Laes Date: Sun, 1 Aug 2010 15:04:09 +0800 Subject: [PATCH 183/207] Added initial version of 'setup.py audit' --- setup.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 78bd25d0..d2a9266b 100644 --- a/setup.py +++ b/setup.py @@ -38,8 +38,44 @@ Links `_ """ -from setuptools import setup +from setuptools import Command, setup +class run_audit(Command): + """Audits source code using PyFlakes for following issues: + - Names which are used but not defined or used before they are defined. + - Names which are redefined without having been used. + """ + description = "Audit source code with PyFlakes" + user_options = [] + + def initialize_options(self): + all = None + + def finalize_options(self): + pass + + def run(self): + import os, sys + try: + import pyflakes.scripts.pyflakes as flakes + except ImportError: + print "Audit requires PyFlakes installed in your system.""" + sys.exit(-1) + + dirs = ['flask', 'tests'] + # Add example directories + for dir in ['flaskr', 'jqueryexample', 'minitwit']: + dirs.append(os.path.join('examples', dir)) + # TODO: Add test subdirectories + warns = 0 + for dir in dirs: + for filename in os.listdir(dir): + if filename.endswith('.py') and filename != '__init__.py': + warns += flakes.checkPath(os.path.join(dir, filename)) + if warns > 0: + print ("Audit finished with total %d warnings." % warns) + else: + print ("No problems found in sourcecode.") def run_tests(): import os, sys @@ -75,5 +111,6 @@ setup( 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' ], + cmdclass={'audit': run_audit}, test_suite='__main__.run_tests' ) From c9002569c9abfa715e491aa636555e51ddf85d50 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 3 Aug 2010 12:15:15 +0200 Subject: [PATCH 184/207] Various pyflakes fixes --- Makefile | 5 ++++- tests/flask_tests.py | 23 +++++++++++------------ tests/flaskext_test.py | 1 - 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index ab0a8d97..a0127457 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,13 @@ -.PHONY: clean-pyc ext-test test upload-docs docs +.PHONY: clean-pyc ext-test test upload-docs docs audit all: clean-pyc test test: python setup.py test +audit: + python setup.py audit + tox-test: PYTHONDONTWRITEBYTECODE= tox diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 010bc42e..2944f0ea 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -15,11 +15,10 @@ import re import sys import flask import unittest -import tempfile from logging import StreamHandler from contextlib import contextmanager from datetime import datetime -from werkzeug import parse_date, parse_options_header, http_date +from werkzeug import parse_date, parse_options_header from werkzeug.exceptions import NotFound from jinja2 import TemplateNotFound from cStringIO import StringIO @@ -352,7 +351,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): called.append(4) return response @app.after_request - def after1(response): + def after2(response): called.append(3) return response @app.route('/') @@ -638,13 +637,13 @@ class ModuleTestCase(unittest.TestCase): app = flask.Flask(__name__) admin = flask.Module(__name__, 'admin', url_prefix='/admin') @admin.route('/') - def index(): + def admin_index(): return 'admin index' @admin.route('/login') - def login(): + def admin_login(): return 'admin login' @admin.route('/logout') - def logout(): + def admin_logout(): return 'admin logout' @app.route('/') def index(): @@ -680,7 +679,7 @@ class ModuleTestCase(unittest.TestCase): catched.append('after-admin') return response @admin.route('/') - def index(): + def admin_index(): return 'the admin' @app.before_request def before_request(): @@ -719,7 +718,7 @@ class ModuleTestCase(unittest.TestCase): def index(): return flask.render_template_string('{{ a }}{{ b }}{{ c }}') @admin.route('/') - def index(): + def admin_index(): return flask.render_template_string('{{ a }}{{ b }}{{ c }}') app.register_module(admin) c = app.test_client() @@ -794,13 +793,13 @@ class ModuleTestCase(unittest.TestCase): f = app.view_functions['admin.static'] try: - rv = f('/etc/passwd') + f('/etc/passwd') except NotFound: pass else: assert 0, 'expected exception' try: - rv = f('../__init__.py') + f('../__init__.py') except NotFound: pass else: @@ -914,7 +913,7 @@ class LoggingTestCase(unittest.TestCase): c = app.test_client() with catch_stderr() as err: - rv = c.get('/') + c.get('/') out = err.getvalue() assert 'WARNING in flask_tests [' in out assert 'flask_tests.py' in out @@ -1098,7 +1097,7 @@ class TestSignals(unittest.TestCase): flask.template_rendered.connect(record, app) try: - rv = app.test_client().get('/') + app.test_client().get('/') assert len(recorded) == 1 template, context = recorded[0] assert template.name == 'simple_template.html' diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py index 9dc9ad67..36a62694 100644 --- a/tests/flaskext_test.py +++ b/tests/flaskext_test.py @@ -18,7 +18,6 @@ import urllib2 import tempfile import subprocess import argparse -from cStringIO import StringIO from flask import json From d17b6d738a0e9f8a3dcf4996a65de6e3347acae6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 3 Aug 2010 12:17:36 +0200 Subject: [PATCH 185/207] Fixed a refacotring error in the docs. This fixes #100 --- docs/testing.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index d131c673..1a3a2960 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -43,13 +43,13 @@ In order to test that, we add a second module ( class FlaskrTestCase(unittest.TestCase): def setUp(self): - self.db_fd, flaskr.DATABASE = tempfile.mkstemp() + self.db_fd, self.app.config['DATABASE'] = tempfile.mkstemp() self.app = flaskr.app.test_client() flaskr.init_db() def tearDown(self): os.close(self.db_fd) - os.unlink(flaskr.DATABASE) + os.unlink(flaskr.app.config['DATABASE']) if __name__ == '__main__': unittest.main() From faa1c71e455a99e9b098aa9bb4667c07a1bab6aa Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 7 Aug 2010 13:02:53 +0200 Subject: [PATCH 186/207] Request local objects now fail properly with a RuntimeError. This fixes #105 --- CHANGES | 2 ++ docs/upgrading.rst | 8 ++++++++ flask/globals.py | 15 +++++++++++---- tests/flask_tests.py | 6 +++++- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 03611cc8..6d0a0172 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,8 @@ Release date to be announced, codename to be selected - Added :meth:`~flask.Flask.make_default_options_response` which can be used by subclasses to alter the default behaviour for `OPTIONS` responses. +- Unbound locals now raise a proper :exc:`RuntimeError` instead + of an :exc:`AttributeError`. Version 0.6.1 ------------- diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 24966e7e..ba8e9947 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -19,6 +19,14 @@ installation, make sure to pass it the ``-U`` parameter:: $ easy_install -U Flask +Version 0.7 +----------- + +Due to a bug in earlier implementations the request local proxies now +raise a :exc:`RuntimeError` instead of an :exc:`AttributeError` when they +are unbound. If you cought these exceptions with :exc:`AttributeError` +before, you should catch them with :exc:`RuntimeError` now. + Version 0.6 ----------- diff --git a/flask/globals.py b/flask/globals.py index aac46555..84714105 100644 --- a/flask/globals.py +++ b/flask/globals.py @@ -10,11 +10,18 @@ :license: BSD, see LICENSE for more details. """ +from functools import partial from werkzeug import LocalStack, LocalProxy +def _lookup_object(name): + top = _request_ctx_stack.top + if top is None: + raise RuntimeError('working outside of request context') + return getattr(top, name) + # context locals _request_ctx_stack = LocalStack() -current_app = LocalProxy(lambda: _request_ctx_stack.top.app) -request = LocalProxy(lambda: _request_ctx_stack.top.request) -session = LocalProxy(lambda: _request_ctx_stack.top.session) -g = LocalProxy(lambda: _request_ctx_stack.top.g) +current_app = LocalProxy(partial(_lookup_object, 'app')) +request = LocalProxy(partial(_lookup_object, 'request')) +session = LocalProxy(partial(_lookup_object, 'session')) +g = LocalProxy(partial(_lookup_object, 'g')) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 2944f0ea..2f90a2f3 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -72,7 +72,7 @@ class ContextTestCase(unittest.TestCase): ctx.pop() try: index() - except AttributeError: + except RuntimeError: pass else: assert 0, 'expected runtime error' @@ -469,6 +469,10 @@ class BasicFunctionalityTestCase(unittest.TestCase): else: assert "Expected ValueError" + def test_request_locals(self): + self.assertEqual(repr(flask.g), '') + self.assertFalse(flask.g) + class JSONTestCase(unittest.TestCase): From fda14678c07d036ef3a1984a4e346e793cc5a63c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 7 Aug 2010 13:36:39 +0200 Subject: [PATCH 187/207] Deprecated send_file etag support and mimetype guessing for file-like objects. This fixes #104 --- CHANGES | 4 ++ docs/upgrading.rst | 17 ++++++++ flask/helpers.py | 27 +++++++++++- tests/flask_tests.py | 100 +++++++++++++++++++++++++++++-------------- 4 files changed, 116 insertions(+), 32 deletions(-) diff --git a/CHANGES b/CHANGES index 6d0a0172..bb5b295d 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,10 @@ Release date to be announced, codename to be selected behaviour for `OPTIONS` responses. - Unbound locals now raise a proper :exc:`RuntimeError` instead of an :exc:`AttributeError`. +- Mimetype guessing and etag support based on file objects is now + deprecated for :func:`flask.send_file` because it was unreliable. + Pass filenames instead or attach your own etags and provide a + proper mimetype by hand. Version 0.6.1 ------------- diff --git a/docs/upgrading.rst b/docs/upgrading.rst index ba8e9947..ac811e41 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -27,6 +27,23 @@ raise a :exc:`RuntimeError` instead of an :exc:`AttributeError` when they are unbound. If you cought these exceptions with :exc:`AttributeError` before, you should catch them with :exc:`RuntimeError` now. +Additionally the :func:`~flask.send_file` function is now issuing +deprecation warnings if you depend on functionality that will be removed +in Flask 1.0. Previously it was possible to use etags and mimetypes +when file objects were passed. This was unreliable and caused issues +for a few setups. If you get a deprecation warning, make sure to +update your application to work with either filenames there or disable +etag attaching and attach them yourself. + +Old code:: + + return send_file(my_file_object) + return send_file(my_file_object) + +New code:: + + return send_file(my_file_object, add_etags=False) + Version 0.6 ----------- diff --git a/flask/helpers.py b/flask/helpers.py index a6c18a33..18eb6d0e 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -259,7 +259,9 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, 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). + to sent certain files as attachment (HTML for instance). The mimetype + guessing requires a `filename` or an `attachment_filename` to be + provided. Please never pass filenames to this function from user sources without checking them first. Something like this is usually sufficient to @@ -274,6 +276,12 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, The `add_etags`, `cache_timeout` and `conditional` parameters were added. The default behaviour is now to attach etags. + .. versionchanged:: 0.7 + mimetype guessing and etag support for file objects was + deprecated because it was unreliable. Pass a filename if you are + able to, otherwise attach an etag yourself. This functionality + will be removed in Flask 1.0 + :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. @@ -295,8 +303,25 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, filename = filename_or_fp file = None else: + from warnings import warn file = filename_or_fp filename = getattr(file, 'name', None) + + # XXX: this behaviour is now deprecated because it was unreliable. + # removed in Flask 1.0 + if not attachment_filename and not mimetype \ + and isinstance(filename, basestring): + warn(DeprecationWarning('The filename support for file objects ' + 'passed to send_file is not deprecated. Pass an ' + 'attach_filename if you want mimetypes to be guessed.'), + stacklevel=2) + if add_etags: + warn(DeprecationWarning('In future flask releases etags will no ' + 'longer be generated for file objects passed to the send_file ' + 'function because this behaviour was unreliable. Pass ' + 'filenames instead if possible, otherwise attach an etag ' + 'yourself based on another value'), stacklevel=2) + if filename is not None: if not os.path.isabs(filename): filename = os.path.join(current_app.root_path, filename) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 2f90a2f3..392368e7 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -33,8 +33,27 @@ TEST_KEY = 'foo' SECRET_KEY = 'devkey' +@contextmanager +def catch_warnings(): + """Catch warnings in a with block in a list""" + import warnings + filters = warnings.filters + warnings.filters = filters[:] + old_showwarning = warnings.showwarning + log = [] + def showwarning(message, category, filename, lineno, file=None, line=None): + log.append(locals()) + try: + warnings.showwarning = showwarning + yield log + finally: + warnings.filters = filters + warnings.showwarning = old_showwarning + + @contextmanager def catch_stderr(): + """Catch stderr in a StringIO""" old_stderr = sys.stderr sys.stderr = rv = StringIO() try: @@ -834,46 +853,64 @@ class SendfileTestCase(unittest.TestCase): 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' + with catch_warnings() as captured: + 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' + # mimetypes + etag + assert len(captured) == 2 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') + with catch_warnings() as captured: + 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') + # mimetypes + etag + assert len(captured) == 2 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' + with catch_warnings() as captured: + f = StringIO('Test') + rv = flask.send_file(f) + assert rv.data == 'Test' + assert rv.mimetype == 'application/octet-stream' + # etags + assert len(captured) == 1 + with catch_warnings() as captured: + f = StringIO('Test') + rv = flask.send_file(f, mimetype='text/plain') + assert rv.data == 'Test' + assert rv.mimetype == 'text/plain' + # etags + assert len(captured) == 1 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 + with catch_warnings() as captured: + with app.test_request_context(): + f = StringIO('Test') + rv = flask.send_file(f) + assert 'x-sendfile' not in rv.headers + # etags + assert len(captured) == 1 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 catch_warnings() as captured: + 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' + # mimetypes + etag + assert len(captured) == 2 with app.test_request_context(): assert options['filename'] == 'index.html' @@ -884,7 +921,8 @@ class SendfileTestCase(unittest.TestCase): with app.test_request_context(): rv = flask.send_file(StringIO('Test'), as_attachment=True, - attachment_filename='index.txt') + attachment_filename='index.txt', + add_etags=False) assert rv.mimetype == 'text/plain' value, options = parse_options_header(rv.headers['Content-Disposition']) assert value == 'attachment' From a354c393aed6d87ad62369cca046f0aff70254c1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 7 Aug 2010 13:37:43 +0200 Subject: [PATCH 188/207] Fixed a typo --- docs/upgrading.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index ac811e41..91da2b78 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -24,7 +24,7 @@ Version 0.7 Due to a bug in earlier implementations the request local proxies now raise a :exc:`RuntimeError` instead of an :exc:`AttributeError` when they -are unbound. If you cought these exceptions with :exc:`AttributeError` +are unbound. If you caught these exceptions with :exc:`AttributeError` before, you should catch them with :exc:`RuntimeError` now. Additionally the :func:`~flask.send_file` function is now issuing From c41a1cd8dca1110ca22da93fb80111b29c7e5740 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 7 Aug 2010 13:38:26 +0200 Subject: [PATCH 189/207] Another typo fix --- docs/patterns/errorpages.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/errorpages.rst b/docs/patterns/errorpages.rst index 95677644..d7852549 100644 --- a/docs/patterns/errorpages.rst +++ b/docs/patterns/errorpages.rst @@ -49,7 +49,7 @@ An error handler is a function, just like a view function, but it is called when an error happens and is passed that error. The error is most likely a :exc:`~werkzeug.exceptions.HTTPException`, but in one case it can be a different error: a handler for internal server errors will be -passed other exception instances as well if they are uncought. +passed other exception instances as well if they are uncaught. An error handler is registered with the :meth:`~flask.Flask.errorhandler` decorator and the error code of the exception. Keep in mind that Flask From 38107c752cf959ad69cf4f35886946bc947f2bd3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 7 Aug 2010 13:41:06 +0200 Subject: [PATCH 190/207] Fixed a wrong import path in the documentation. Fixes #102 --- docs/errorhandling.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index 82d53b7c..c216c160 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -89,7 +89,7 @@ There are a couple of handlers provided by the logging system out of the box but not all of them are useful for basic error logging. The most interesting are probably the following: -- :class:`~logging.handlers.FileHandler` - logs messages to a file on the +- :class:`~logging.FileHandler` - logs messages to a file on the filesystem. - :class:`~logging.handlers.RotatingFileHandler` - logs messages to a file on the filesystem and will rotate after a certain number of messages. @@ -105,7 +105,7 @@ above, just make sure to use a lower setting (I would recommend if not app.debug: import logging - from logging.handlers import TheHandlerYouWant + from themodule import TheHandler YouWant file_handler = TheHandlerYouWant(...) file_handler.setLevel(logging.WARNING) app.logger.addHandler(file_handler) From a3a843999b8f98a96fb44f573515d44648d72bab Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 9 Aug 2010 15:16:02 +0200 Subject: [PATCH 191/207] normpath is now used before loading templates --- CHANGES | 3 +++ flask/templating.py | 4 ++++ tests/flask_tests.py | 2 ++ tests/moduleapp/apps/admin/__init__.py | 5 +++++ 4 files changed, 14 insertions(+) diff --git a/CHANGES b/CHANGES index bb5b295d..c20a3304 100644 --- a/CHANGES +++ b/CHANGES @@ -25,6 +25,9 @@ Bugfix release, release date to be announced. - Fixed an issue where the default `OPTIONS` response was not exposing all valid methods in the `Allow` header. +- Jinja2 template loading syntax now allows "./" in front of + a template load path. Previously this caused issues with + module setups. Version 0.6 ----------- diff --git a/flask/templating.py b/flask/templating.py index db78c3af..4db03b75 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -8,6 +8,7 @@ :copyright: (c) 2010 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +import posixpath from jinja2 import BaseLoader, TemplateNotFound from .globals import _request_ctx_stack @@ -36,6 +37,9 @@ class _DispatchingJinjaLoader(BaseLoader): self.app = app def get_source(self, environment, template): + template = posixpath.normpath(template) + if template.startswith('../'): + raise TemplateNotFound(template) loader = None try: module, name = template.split('/', 1) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 392368e7..ae972d05 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -789,6 +789,8 @@ class ModuleTestCase(unittest.TestCase): assert rv.data == 'Hello from the Frontend' rv = c.get('/admin/') assert rv.data == 'Hello from the Admin' + rv = c.get('/admin/index2') + assert rv.data == 'Hello from the Admin' rv = c.get('/admin/static/test.txt') assert rv.data.strip() == 'Admin File' rv = c.get('/admin/static/css/test.css') diff --git a/tests/moduleapp/apps/admin/__init__.py b/tests/moduleapp/apps/admin/__init__.py index 98af2b26..b85b8024 100644 --- a/tests/moduleapp/apps/admin/__init__.py +++ b/tests/moduleapp/apps/admin/__init__.py @@ -7,3 +7,8 @@ admin = Module(__name__, url_prefix='/admin') @admin.route('/') def index(): return render_template('admin/index.html') + + +@admin.route('/index2') +def index2(): + return render_template('./admin/index.html') From 9a21c34bb63c2343b7c8c6bcfc51fc72030909ed Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 10 Aug 2010 22:55:30 +0200 Subject: [PATCH 192/207] Added another testcase --- tests/flask_tests.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index ae972d05..b850dc8c 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -248,7 +248,13 @@ class BasicFunctionalityTestCase(unittest.TestCase): flask.session['test'] = 42 flask.session.permanent = permanent return '' - rv = app.test_client().get('/') + + @app.route('/test') + def test(): + return unicode(flask.session.permanent) + + client = app.test_client() + rv = client.get('/') assert 'set-cookie' in rv.headers match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) expires = parse_date(match.group()) @@ -257,6 +263,9 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert expires.month == expected.month assert expires.day == expected.day + rv = client.get('/test') + assert rv.data == 'True' + permanent = False rv = app.test_client().get('/') assert 'set-cookie' in rv.headers From 6b5ba145213d49078dc03f3f62fb3ad5aa139e68 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 10 Aug 2010 23:10:55 +0200 Subject: [PATCH 193/207] Added a missing is --- docs/patterns/flashing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/flashing.rst b/docs/patterns/flashing.rst index b491712f..4cea0206 100644 --- a/docs/patterns/flashing.rst +++ b/docs/patterns/flashing.rst @@ -14,7 +14,7 @@ template that does this. Simple Flashing --------------- -So here a full example:: +So here is a full example:: from flask import flash, redirect, url_for, render_template From a3f78af87019bfd89063cf5bc0c19f27325f36c1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 13 Aug 2010 23:37:57 +0200 Subject: [PATCH 194/207] Improved the templated decorator in the documentation as recommended by Thadeus Burgess on the mailinglist --- docs/patterns/viewdecorators.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/patterns/viewdecorators.rst b/docs/patterns/viewdecorators.rst index 49620ff8..73d67852 100644 --- a/docs/patterns/viewdecorators.rst +++ b/docs/patterns/viewdecorators.rst @@ -120,7 +120,9 @@ As you can see, if no template name is provided it will use the endpoint of the URL map with dots converted to slashes + ``'.html'``. Otherwise the provided template name is used. When the decorated function returns, the dictionary returned is passed to the template rendering function. If -`None` is returned, an empty dictionary is assumed. +`None` is returned, an empty dictionary is assumed, if something else than +a dictionary is returned we return it from the function unchanged. That +way you can still use the redirect function or return simple strings. Here the code for that decorator:: @@ -138,6 +140,8 @@ Here the code for that decorator:: ctx = f(*args, **kwargs) if ctx is None: ctx = {} + elif not isinstance(ctx, dict): + return ctx return render_template(template_name, **ctx) return decorated_function return decorator From 36a421bb3a235f696131bc8a546492fc04a411f0 Mon Sep 17 00:00:00 2001 From: Zhao Xiaohong Date: Sun, 15 Aug 2010 10:52:11 +0800 Subject: [PATCH 195/207] Fixed template_rendered example in signal documentation. --- docs/signals.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/signals.rst b/docs/signals.rst index feb9a7b2..73a0c744 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -55,7 +55,7 @@ to the template:: @contextmanager def captured_templates(app): recorded = [] - def record(template, context): + def record(sender, template, context): recorded.append((template, context)) template_rendered.connect(record, app) try: @@ -87,7 +87,7 @@ its own which simplifies the example above:: def captured_templates(app): recorded = [] - def record(template, context): + def record(sender, template, context): recorded.append((template, context)) return template_rendered.connected_to(record, app) @@ -155,7 +155,7 @@ With Blinker 1.1 you can also easily subscribe to signals by using the new from flask import template_rendered @template_rendered.connect_via(app) - def when_template_rendered(template, context): + def when_template_rendered(sender, template, context): print 'Template %s is rendered with %s' % (template.name, context) Core Signals From 0566abc28da9c997a9f7ac7b38066075a7a81cd8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 18 Aug 2010 20:10:47 +0200 Subject: [PATCH 196/207] Fixed testing example --- docs/testing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/testing.rst b/docs/testing.rst index 1a3a2960..790eddf6 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -43,7 +43,7 @@ In order to test that, we add a second module ( class FlaskrTestCase(unittest.TestCase): def setUp(self): - self.db_fd, self.app.config['DATABASE'] = tempfile.mkstemp() + self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() self.app = flaskr.app.test_client() flaskr.init_db() From d903f55e835dd143afb6e0079376525718ffb5a9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 18 Aug 2010 21:58:04 +0200 Subject: [PATCH 197/207] s/sdist/dist/. This fixes #106 --- docs/patterns/fabric.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/fabric.rst b/docs/patterns/fabric.rst index de23ffa8..49be85ab 100644 --- a/docs/patterns/fabric.rst +++ b/docs/patterns/fabric.rst @@ -50,7 +50,7 @@ virtual environment:: # figure out the release name and version dist = local('python setup.py --fullname').strip() # upload the source tarball to the temporary folder on the server - put('sdist/%s.tar.gz' % dist, '/tmp/yourapplication.tar.gz') + put('dist/%s.tar.gz' % dist, '/tmp/yourapplication.tar.gz') # create a place where we can unzip the tarball, then enter # that directory and unzip it run('mkdir yourapplication') From 6fca662c841797a2006999c699a6738b20af1d4e Mon Sep 17 00:00:00 2001 From: Heungsub Lee Date: Fri, 20 Aug 2010 15:29:13 +0800 Subject: [PATCH 198/207] Fix the 108th issue. --- flask/module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/module.py b/flask/module.py index 29350eb6..5f3c8c1f 100644 --- a/flask/module.py +++ b/flask/module.py @@ -31,7 +31,8 @@ def _register_module(module, static_path): path = state.url_prefix + path state.app.add_url_rule(path + '/', endpoint='%s.static' % module.name, - view_func=module.send_static_file) + view_func=module.send_static_file, + subdomain=module.subdomain) return _register From 2a73bbc436041875f81aeffd5b8f13c6da8fcf19 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 20 Aug 2010 11:16:18 +0200 Subject: [PATCH 199/207] Added testcase. This fixes #108 --- tests/flask_tests.py | 9 +++++++++ tests/subdomaintestmodule/__init__.py | 4 ++++ tests/subdomaintestmodule/static/hello.txt | 1 + 3 files changed, 14 insertions(+) create mode 100644 tests/subdomaintestmodule/__init__.py create mode 100644 tests/subdomaintestmodule/static/hello.txt diff --git a/tests/flask_tests.py b/tests/flask_tests.py index b850dc8c..8eecafc0 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1102,6 +1102,15 @@ class SubdomainTestCase(unittest.TestCase): rv = c.get('/', 'http://test.localhost/') assert rv.data == 'test index' + def test_module_static_path_subdomain(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'example.com' + from subdomaintestmodule import mod + app.register_module(mod) + c = app.test_client() + rv = c.get('/static/hello.txt', 'http://foo.example.com/') + assert rv.data.strip() == 'Hello Subdomain' + def test_subdomain_matching(self): app = flask.Flask(__name__) app.config['SERVER_NAME'] = 'localhost' diff --git a/tests/subdomaintestmodule/__init__.py b/tests/subdomaintestmodule/__init__.py new file mode 100644 index 00000000..3c5e3583 --- /dev/null +++ b/tests/subdomaintestmodule/__init__.py @@ -0,0 +1,4 @@ +from flask import Module + + +mod = Module(__name__, 'foo', subdomain='foo') diff --git a/tests/subdomaintestmodule/static/hello.txt b/tests/subdomaintestmodule/static/hello.txt new file mode 100644 index 00000000..12e23c16 --- /dev/null +++ b/tests/subdomaintestmodule/static/hello.txt @@ -0,0 +1 @@ +Hello Subdomain From 6e3dd9b3ce3ddb6ae1689654ce6dc2e71742c8a7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 20 Aug 2010 11:20:09 +0200 Subject: [PATCH 200/207] Added a changelog entry for #108 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index c20a3304..b33d629d 100644 --- a/CHANGES +++ b/CHANGES @@ -28,6 +28,8 @@ Bugfix release, release date to be announced. - Jinja2 template loading syntax now allows "./" in front of a template load path. Previously this caused issues with module setups. +- Fixed an issue where the subdomain setting for modules was + ignored for the static folder. Version 0.6 ----------- From c62422c3181f2f8332893b06ee83581d63bceac8 Mon Sep 17 00:00:00 2001 From: agentultra Date: Fri, 10 Sep 2010 03:33:20 +0800 Subject: [PATCH 201/207] Updated documentation on packaging patterns --- docs/patterns/packages.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index cd2deac1..35033b59 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -212,6 +212,7 @@ static files and templates. Imagine you have an application like this:: /yourapplication __init__.py /apps + __init__.py /frontend __init__.py views.py @@ -244,6 +245,21 @@ name of the module. So for the admin it would be possible to refer to templates without the prefixed module name. This is explicit unlike URL rules. +You also need to explicitly pass the ``url_prefix`` argument when +registering your modules this way:: + + # in yourapplication/__init__.py + from flask import Flask + from yourapplication.apps.admin.views import admin + from yourapplication.apps.frontend.views import frontend + + + app = Flask(__name__) + app.register_module(admin, url_prefix='/admin') + app.register_module(frontend, url_prefix='/frontend') + +This is because Flask cannot infer the prefix from the package names. + .. admonition:: References to Static Folders Please keep in mind that if you are using unqualified endpoints by From eb67242e1cb08566a1cd2489933479368a48b4a5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 10 Sep 2010 11:01:58 -0700 Subject: [PATCH 202/207] Fixed a typo in a docstring in app.py --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index f32e4dd8..964394a6 100644 --- a/flask/app.py +++ b/flask/app.py @@ -238,7 +238,7 @@ class Flask(_PackageBoundObject): #: this function is active for, `None` for all requests. This can for #: example be used to open database connections or getting hold of the #: currently logged in user. To register a function here, use the - #: :meth:`before_request` decorator. + #: :meth:`after_request` decorator. self.after_request_funcs = {} #: A dictionary with list of functions that are called without argument From 1e4e578d73b78e2e11696b2e2d0763fd00367521 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 12 Sep 2010 13:18:08 -0700 Subject: [PATCH 203/207] Added the extensions dictionary on the application --- flask/app.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/flask/app.py b/flask/app.py index 964394a6..dbb64646 100644 --- a/flask/app.py +++ b/flask/app.py @@ -256,6 +256,22 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.5 self.modules = {} + #: a place where extensions can store application specific state. For + #: example this is where an extension could store database engines and + #: similar things. For backwards compatibility extensions should register + #: themselves like this:: + #: + #: if not hasattr(app, 'extensions'): + #: app.extensions = {} + #: app.extensions['extensionname'] = SomeObject() + #: + #: The key must match the name of the `flaskext` module. For example in + #: case of a "Flask-Foo" extension in `flaskext.foo`, the key would be + #: ``'foo'``. + #: + #: .. versionadded:: 0.7 + self.extensions = {} + #: The :class:`~werkzeug.routing.Map` for this instance. You can use #: this to change the routing converters after the class was created #: but before any routes are connected. Example:: From 216478f715b330e7d9298c6a11d3e5c0a1e790f8 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Wed, 6 Oct 2010 09:47:48 +0800 Subject: [PATCH 204/207] docs: Finished sentence on Notes on Proxies. --- docs/api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index ed2a5037..00b5105e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -421,7 +421,7 @@ Notes On Proxies ---------------- Some of the objects provided by Flask are proxies to other objects. The -reason behind this is, that these proxies are shared between threads and +reason behind this is that these proxies are shared between threads and they have to dispatch to the actual object bound to a thread behind the scenes as necessary. @@ -430,7 +430,7 @@ exceptions where it is good to know that this object is an actual proxy: - The proxy objects do not fake their inherited types, so if you want to perform actual instance checks, you have to do that on the instance - that + that is being proxied (see `_get_current_object` below). - if the object reference is important (so for example for sending :ref:`signals`) From 6875a057ec97fb17927b951cd1e5baeef2cf81d8 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Wed, 6 Oct 2010 14:05:35 +0800 Subject: [PATCH 205/207] Fixed small typos in docs. Added a cross-ref. --- docs/becomingbig.rst | 2 +- docs/config.rst | 4 ++-- docs/deploying/cgi.rst | 2 +- docs/deploying/fastcgi.rst | 2 +- docs/deploying/mod_wsgi.rst | 4 ++-- docs/deploying/others.rst | 4 ++-- docs/extensiondev.rst | 2 +- docs/license.rst | 2 +- docs/patterns/errorpages.rst | 2 +- docs/patterns/fileuploads.rst | 6 +++--- docs/patterns/flashing.rst | 4 ++-- docs/patterns/lazyloading.rst | 2 +- docs/patterns/mongokit.rst | 2 +- docs/patterns/sqlite3.rst | 2 +- docs/patterns/viewdecorators.rst | 2 +- docs/styleguide.rst | 6 +++--- docs/templating.rst | 2 +- docs/tutorial/setup.rst | 2 +- docs/upgrading.rst | 2 +- 19 files changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst index 6c95c6e2..20a0186e 100644 --- a/docs/becomingbig.rst +++ b/docs/becomingbig.rst @@ -10,7 +10,7 @@ application there are ways to deal with that. Flask is powered by Werkzeug and Jinja2, two libraries that are in use at a number of large websites out there and all Flask does is bring those two together. Being a microframework Flask does not do much more than -combinding existing libraries - there is not a lot of code involved. +combining existing libraries - there is not a lot of code involved. What that means for large applications is that it's very easy to take the code from Flask and put it into a new module within the applications and expand on that. diff --git a/docs/config.rst b/docs/config.rst index e782bc7f..1c2648a5 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -61,7 +61,7 @@ The following configuration values are used internally by Flask: ``USE_X_SENDFILE`` enable/disable x-sendfile ``LOGGER_NAME`` the name of the logger ``SERVER_NAME`` the name of the server. Required for - subdomain support (eg: ``'localhost'``) + subdomain support (e.g.: ``'localhost'``) ``MAX_CONTENT_LENGTH`` If set to a value in bytes, Flask will reject incoming requests with a content length greater than this by @@ -222,7 +222,7 @@ your configuration files. However here a list of good recommendations: even create your own script for sourcing that activates a virtualenv and exports the development configuration for you. - Use a tool like `fabric`_ in production to push code and - configurations sepearately to the production server(s). For some + configurations separately to the production server(s). For some details about how to do that, head over to the :ref:`deploy` pattern. .. _fabric: http://fabfile.org/ diff --git a/docs/deploying/cgi.rst b/docs/deploying/cgi.rst index c0b8c560..5d5b085c 100644 --- a/docs/deploying/cgi.rst +++ b/docs/deploying/cgi.rst @@ -38,7 +38,7 @@ Server Setup ------------ Usually there are two ways to configure the server. Either just copy the -`.cgi` into a `cgi-bin` (and use `mod_rerwite` or something similar to +`.cgi` into a `cgi-bin` (and use `mod_rewrite` or something similar to rewrite the URL) or let the server point to the file directly. In Apache for example you can put a like like this into the config: diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index a29664e8..19bd42ec 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -123,7 +123,7 @@ webserver user is `www-data`:: $ cd /var/www/yourapplication $ python application.fcgi Traceback (most recent call last): - File "yourapplication.fcg", line 4, in + File "yourapplication.fcgi", line 4, in ImportError: No module named yourapplication In this case the error seems to be "yourapplication" not being on the python diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index 5b19f1d5..40df522d 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -93,8 +93,8 @@ For more information consult the `mod_wsgi wiki`_. .. _virtual python: http://pypi.python.org/pypi/virtualenv .. _mod_wsgi wiki: http://code.google.com/p/modwsgi/wiki/ -Toubleshooting --------------- +Troubleshooting +--------------- If your application does not run, follow this guide to troubleshoot: diff --git a/docs/deploying/others.rst b/docs/deploying/others.rst index 793a4bed..8f08ecd1 100644 --- a/docs/deploying/others.rst +++ b/docs/deploying/others.rst @@ -69,10 +69,10 @@ If you deploy your application behind an HTTP proxy you will need to rewrite a few headers in order for the application to work. The two problematic values in the WSGI environment usually are `REMOTE_ADDR` and `HTTP_HOST`. Werkzeug ships a fixer that will solve some common setups, -but you might want to write your own WSGI middlware for specific setups. +but you might want to write your own WSGI middleware for specific setups. The most common setup invokes the host being set from `X-Forwarded-Host` -and the remote address from `X-Forwared-For`:: +and the remote address from `X-Forward-For`:: from werkzeug.contrib.fixers import ProxyFix app.wsgi_app = ProxyFix(app.wsgi_app) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index cfad85f0..1848ca8f 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -153,7 +153,7 @@ There are two recommended ways for an extension to initialize: initialization functions: If your extension is called `helloworld` you might have a function - called ``init_helloworld(app[, extra_args])`` that initalizes the + called ``init_helloworld(app[, extra_args])`` that initializes the extension for that application. It could attach before / after handlers etc. diff --git a/docs/license.rst b/docs/license.rst index 62e5c75e..38777e66 100644 --- a/docs/license.rst +++ b/docs/license.rst @@ -4,7 +4,7 @@ License Flask is licensed under a three clause BSD License. It basically means: do whatever you want with it as long as the copyright in Flask sticks around, the conditions are not modified and the disclaimer is present. -Furthermore you must not use the names of the authors to promote derivates +Furthermore you must not use the names of the authors to promote derivatives of the software without written consent. The full license text can be found below (:ref:`flask-license`). For the diff --git a/docs/patterns/errorpages.rst b/docs/patterns/errorpages.rst index d7852549..4041bd8a 100644 --- a/docs/patterns/errorpages.rst +++ b/docs/patterns/errorpages.rst @@ -33,7 +33,7 @@ even if the application behaves correctly: instead of 404. If you are not deleting documents permanently from the database but just mark them as deleted, do the user a favour and use the 410 code instead and display a message that what he was - looking for was deleted for all ethernity. + looking for was deleted for all eternity. *500 Internal Server Error* Usually happens on programming errors or if the server is overloaded. diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst index 99f009c7..221ce327 100644 --- a/docs/patterns/fileuploads.rst +++ b/docs/patterns/fileuploads.rst @@ -41,9 +41,9 @@ the URL to these files. Why do we limit the extensions that are allowed? You probably don't want your users to be able to upload everything there if the server is directly sending out the data to the client. That way you can make sure that users -are not able to upload HTML files that would cause XSS problems. Also -make sure to disallow `.php` files if the server executes them, but who -has PHP installed on his server, right? :) +are not able to upload HTML files that would cause XSS problems (see +:ref:`xss`). Also make sure to disallow `.php` files if the server +executes them, but who has PHP installed on his server, right? :) Next the functions that check if an extension is valid and that uploads the file and redirects the user to the URL for the uploaded file:: diff --git a/docs/patterns/flashing.rst b/docs/patterns/flashing.rst index 4cea0206..3610944e 100644 --- a/docs/patterns/flashing.rst +++ b/docs/patterns/flashing.rst @@ -30,7 +30,7 @@ So here is a full example:: request.form['password'] != 'secret': error = 'Invalid credentials' else: - flash('You were sucessfully logged in') + flash('You were successfully logged in') return redirect(url_for('index')) return render_template('login.html', error=error) @@ -100,7 +100,7 @@ to the :func:`~flask.flash` function:: Inside the template you then have to tell the :func:`~flask.get_flashed_messages` function to also return the -categories. The loop looks slighty different in that situation then: +categories. The loop looks slightly different in that situation then: .. sourcecode:: html+jinja diff --git a/docs/patterns/lazyloading.rst b/docs/patterns/lazyloading.rst index 03b293d8..50ad6fa8 100644 --- a/docs/patterns/lazyloading.rst +++ b/docs/patterns/lazyloading.rst @@ -100,5 +100,5 @@ name and a dot, and by wrapping `view_func` in a `LazyView` as needed:: url('/user/', 'views.user') One thing to keep in mind is that before and after request handlers have -to be in a file that is imported upfront to work propery on the first +to be in a file that is imported upfront to work properly on the first request. The same goes for any kind of remaining decorator. diff --git a/docs/patterns/mongokit.rst b/docs/patterns/mongokit.rst index c1727c80..a9c4eef5 100644 --- a/docs/patterns/mongokit.rst +++ b/docs/patterns/mongokit.rst @@ -78,7 +78,7 @@ validator for the maximum character length and uses a special MongoKit feature called `use_dot_notation`. Per default MongoKit behaves like a python dictionary but with `use_dot_notation` set to `True` you can use your documents like you use models in nearly any other ORM by using dots to -seperate between attributes. +separate between attributes. You can insert entries into the database like this: diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index 1032097e..68833234 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -61,7 +61,7 @@ Or if you just want a single result:: To pass variable parts to the SQL statement, use a question mark in the statement and pass in the arguments as a list. Never directly add them to -the SQL statement with string formattings because this makes it possible +the SQL statement with string formatting because this makes it possible to attack the application using `SQL Injections `_. diff --git a/docs/patterns/viewdecorators.rst b/docs/patterns/viewdecorators.rst index 73d67852..c61f1a4b 100644 --- a/docs/patterns/viewdecorators.rst +++ b/docs/patterns/viewdecorators.rst @@ -89,7 +89,7 @@ Here the code:: return decorated_function return decorator -Notice that this assumes an instanciated `cache` object is available, see +Notice that this assumes an instantiated `cache` object is available, see :ref:`caching-pattern` for more information. diff --git a/docs/styleguide.rst b/docs/styleguide.rst index ec699052..0fdc88d8 100644 --- a/docs/styleguide.rst +++ b/docs/styleguide.rst @@ -72,7 +72,7 @@ Expressions and Statements General whitespace rules: - No whitespace for unary operators that are not words - (eg: ``-``, ``~`` etc.) as well on the inner side of parentheses. + (e.g.: ``-``, ``~`` etc.) as well on the inner side of parentheses. - Whitespace is placed between binary operators. Good:: @@ -151,7 +151,7 @@ Docstrings Docstring conventions: All docstrings are formatted with reStructuredText as understood by Sphinx. Depending on the number of lines in the docstring, they are - layed out differently. If it's just one line, the closing triple + laid out differently. If it's just one line, the closing triple quote is on the same line as the opening, otherwise the text is on the same line as the opening quote and the triple quote that closes the string on its own line:: @@ -162,7 +162,7 @@ Docstring conventions: def bar(): """This is a longer docstring with so much information in there - that it spans three lines. In this case the closing tripple quote + that it spans three lines. In this case the closing triple quote is on its own line. """ diff --git a/docs/templating.rst b/docs/templating.rst index 2583cc2c..bd940b0e 100644 --- a/docs/templating.rst +++ b/docs/templating.rst @@ -144,7 +144,7 @@ autoescape %}`` block:

    {{ will_not_be_escaped }} {% endautoescape %} -Whenever you do this, please be very cautious about the varibles you are +Whenever you do this, please be very cautious about the variables you are using in this block. Registering Filters diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst index b5ae2a0e..9f762c84 100644 --- a/docs/tutorial/setup.rst +++ b/docs/tutorial/setup.rst @@ -76,7 +76,7 @@ focus on that a little later. First we should get the database working. .. admonition:: Externally Visible Server - Want your server to be publically available? Check out the + Want your server to be publicly available? Check out the :ref:`externally visible server ` section for more information. diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 91da2b78..17523290 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -52,7 +52,7 @@ order of after-request handlers. Previously they were called in the order of the registration, now they are called in reverse order. This change was made so that Flask behaves more like people expected it to work and how other systems handle request pre- and postprocessing. If you -dependend on the order of execution of post-request functions, be sure to +depend on the order of execution of post-request functions, be sure to change the order. Another change that breaks backwards compatibility is that context From 085faf2a776020c9422bf57b5e37b05705d6b95f Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Wed, 6 Oct 2010 14:06:46 +0800 Subject: [PATCH 206/207] First pass to reword security doc for word flow. --- docs/security.rst | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/security.rst b/docs/security.rst index f3193d62..24a4ceb2 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -62,33 +62,33 @@ Another big problem is CSRF. This is a very complex topic and I won't outline it here in detail just mention what it is and how to theoretically prevent it. -So if your authentication information is stored in cookies you have -implicit state management. By that I mean that the state of "being logged -in" is controlled by a cookie and that cookie is sent with each request to -a page. Unfortunately that really means "each request" so also requests -triggered by 3rd party sites. If you don't keep that in mind some people -might be able to trick your application's users with social engineering to -do stupid things without them knowing. +If your authentication information is stored in cookies, you have implicit +state management. The state of "being logged in" is controlled by a +cookie, and that cookie is sent with each request to a page. +Unfortunately that includes requests triggered by 3rd party sites. If you +don't keep that in mind, some people might be able to trick your +application's users with social engineering to do stupid things without +them knowing. Say you have a specific URL that, when you sent `POST` requests to will delete a user's profile (say `http://example.com/user/delete`). If an attacker now creates a page that sends a post request to that page with -some JavaScript he just has to trick some users to that page and their -profiles will end up being deleted. +some JavaScript he just has to trick some users to load that page and +their profiles will end up being deleted. -Imagine you would run Facebook with millions of concurrent users and -someone would send out links to images of little kittens. When a user -would go to that page their profiles would get deleted while they are +Imagine you were to run Facebook with millions of concurrent users and +someone would send out links to images of little kittens. When users +would go to that page, their profiles would get deleted while they are looking at images of fluffy cats. -So how can you prevent yourself from that? Basically for each request -that modifies content on the server you would have to either use a -one-time token and store that in the cookie **and** also transmit it with -the form data. After recieving the data on the server again you would -then have to compare the two tokens and ensure they are equal. +How can you prevent that? Basically for each request that modifies +content on the server you would have to either use a one-time token and +store that in the cookie **and** also transmit it with the form data. +After receiving the data on the server again, you would then have to +compare the two tokens and ensure they are equal. -Why does not Flask do that for you? The ideal place for this to happen is -the form validation framework which does not exist in Flask. +Why does Flask not do that for you? The ideal place for this to happen is +the form validation framework, which does not exist in Flask. .. _json-security: @@ -111,8 +111,8 @@ generate JSON. So what is the issue and how to avoid it? The problem are arrays at toplevel in JSON. Imagine you send the following data out in a JSON -request. Say that's exporting the names and email adresses of all your -friends for a part of the userinterface that is written in JavaScript. +request. Say that's exporting the names and email addresses of all your +friends for a part of the user interface that is written in JavaScript. Not very uncommon: .. sourcecode:: javascript From 325b96099a7221d54b57429d505aaae4d9f67400 Mon Sep 17 00:00:00 2001 From: agentultra Date: Wed, 6 Oct 2010 21:55:42 +0800 Subject: [PATCH 207/207] Small change to the packages documentation example for clarity --- docs/patterns/packages.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 35033b59..4ec09e6b 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -162,9 +162,14 @@ modules in the application (`__init__.py`) like this:: from yourapplication.views.frontend import frontend app = Flask(__name__) - app.register_module(admin) + app.register_module(admin, url_prefix='/admin') app.register_module(frontend) +We register the modules with the app so that it can add them to the +URL map for our application. Note the prefix argument to the admin +module: by default when we register a module, that module's end-points +will be registered on `/` unless we specify this argument. + So what is different when working with modules? It mainly affects URL generation. Remember the :func:`~flask.url_for` function? When not working with modules it accepts the name of the function as first