Permalink
Browse files

Initial Commit

  • Loading branch information...
1 parent 3e1b632 commit aa4704b00a09da5914e30c65a36b93623cd531f0 @apiguy committed Oct 30, 2012
Showing with 295 additions and 0 deletions.
  1. +4 −0 .gitignore
  2. +32 −0 LICENSE
  3. +142 −0 flask_classy.py
  4. +34 −0 setup.py
  5. +83 −0 test_classy.py
View
@@ -25,3 +25,7 @@ pip-log.txt
#Mr Developer
.mr.developer.cfg
+
+#PyCharm
+.idea/*
+.idea/libraries/sass_stdlib.xml
View
@@ -0,0 +1,32 @@
+Copyright (c) 2012 by Freedom Dumlao.
+
+Some rights reserved.
+
+Redistribution and use in source and binary forms of the software as well
+as documentation, with or without modification, are permitted provided
+that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+* The names of the contributors may not be used to endorse or
+ promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE AND 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
+SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
View
@@ -0,0 +1,142 @@
+"""
+ Flask-Classy
+ ------------
+
+ Class based views for the Flask microframework.
+
+ :copyright: (c) 2012 by Freedom Dumlao.
+ :license: BSD, see LICENSE for more details.
+"""
+
+import inspect
+
+def route(rule, **options):
+ """A decorator that is used to define custom routes for methods in
+ FlaskView subclasses. The format is exactly the same as Flask's
+ `@app.route` decorator.
+ """
+ def decorator(f):
+ f.route = (rule, options)
+ return f
+
+ return decorator
+
+class FlaskView(object):
+ """Base view for any class based views implemented with Flask-Classy. Will
+ automatically configure routes when registered with a Flask app instance.
+ """
+
+ @classmethod
+ def register(cls, app, route_base=None):
+ """Registers a FlaskView class for use with a specific instance of a
+ Flask app. Any methods not prefixes with an underscore are candidates
+ to be routed and will have routes registered when this method is
+ called.
+
+ :param app: an instance of a Flask application
+
+ :param route_base: the base path to use for all routes registered for
+ this class
+ """
+
+ if cls is FlaskView:
+ raise TypeError("cls must be a subclass of FlaskVew, not FlaskView itself")
+
+ if route_base:
+ cls.route_base = route_base
+
+ members = cls.find_member_methods()
+ special_methods = ["get", "put", "post", "delete", "index"]
+ id_methods = ["get", "put", "delete"]
+
+ for name, value in members:
+ proxy = cls.make_proxy_method(name)
+ route_name = cls.build_route_name(name)
+
+ if hasattr(value, "route"):
+ rule, options = value.route
+ rule = cls.build_rule(rule)
+ app.add_url_rule(rule, route_name, proxy, **options)
+
+ elif name in special_methods:
+ methods = None
+ if name in ["get", "index"]:
+ methods = ["GET"]
+ elif name == "put":
+ methods = ["PUT"]
+ elif name == "post":
+ methods = ["POST"]
+ elif name == "delete":
+ methods = ["DELETE"]
+
+ if name in id_methods:
+ rule = "/<id>/"
+ else:
+ rule = "/"
+ rule = cls.build_rule(rule)
+ app.add_url_rule(rule, route_name, proxy, methods=methods)
+
+ else:
+ rule = cls.build_rule('/%s/' % name)
+ app.add_url_rule(rule, route_name, proxy)
+
+ @classmethod
+ def make_proxy_method(cls, name):
+ """Creates a proxy function that can be used by Flasks routing. The
+ proxy instantiates the FlaskView subclass and calls the appropriate
+ method.
+
+ :param name: the name of the method to create a proxy for
+ """
+
+ def proxy(*args, **kwargs):
+ i = cls()
+ m = getattr(i, name)
+ return m(*args, **kwargs)
+ return proxy
+
+ @classmethod
+ def find_member_methods(cls):
+ """Returns a list of methods that can be routed to"""
+
+ base_members = dir(FlaskView)
+ all_members = inspect.getmembers(cls, predicate=inspect.ismethod)
+ return [member for member in all_members
+ if not member[0] in base_members
+ and not member[0].startswith("_")]
+
+ @classmethod
+ def build_rule(cls, rule):
+ """Creates a routing rule based on either the class name (minus the
+ 'View' suffix) or the defined `route_base` attribute of the class
+
+ :param rule: the path portion that should be appended to the
+ route base
+ """
+ if hasattr(cls, "route_base"):
+ route_base = cls.route_base
+ else:
+ if cls.__name__.endswith("View"):
+ route_base = cls.__name__[:-4].lower()
+ else:
+ route_base = cls.__name__.lower()
+
+ if not route_base.startswith("/"):
+ route_base = "/" + route_base
+
+ if route_base.endswith("/") and rule.startswith("/"):
+ rule = rule.lstrip("/")
+
+ return route_base + rule
+
+ @classmethod
+ def build_route_name(cls, method_name):
+ """Creates a unique route name based on the combination of the class
+ name with the method name.
+
+ :param method_name: the method name to use when building a route name
+ """
+ return cls.__name__ + ":%s" % method_name
+
+
+
View
@@ -0,0 +1,34 @@
+"""
+Flask-Classy
+-------------
+
+Class based views for Flask
+"""
+from setuptools import setup
+
+setup(
+ name='Flask-Classy',
+ version='0.1',
+ url='https://github.com/apiguy/flask-classy',
+ license='BSD',
+ author='Freedom Dumlao',
+ author_email='freedomdumlao@gmail.com',
+ description='Class based views for Flask',
+ long_description=__doc__,
+ py_modules=['flask_classy'],
+ zip_safe=False,
+ include_package_data=True,
+ platforms='any',
+ install_requires=[
+ 'Flask'
+ ],
+ classifiers=[
+ 'Environment :: Web Environment',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
+ 'Topic :: Software Development :: Libraries :: Python Modules'
+ ]
+)
View
@@ -0,0 +1,83 @@
+import unittest
+from flask import Flask
+from flask_classy import FlaskView, route
+
+class BasicTestView(FlaskView):
+
+ def index(self):
+ return "Index"
+
+ def get(self, id):
+ return "Get " + id
+
+ def put(self, id):
+ return "Put " + id
+
+ def post(self):
+ return "Post"
+
+ def delete(self, id):
+ return "Delete " + id
+
+ def other_method(self):
+ return "Other Method"
+
+ @route("/another")
+ def another_method(self):
+ return "Another Method"
+
+class IndexTestView(FlaskView):
+ route_base = "/"
+
+ def index(self):
+ return "Home Page"
+
+class CommonTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.app = Flask(__name__)
+ self.client = self.app.test_client()
+ BasicTestView.register(self.app)
+ IndexTestView.register(self.app)
+
+ def test_basic_index(self):
+ res = self.client.get("/basictest/")
+ self.assertEqual("Index", res.data)
+
+ def test_basic_get(self):
+ res = self.client.get("/basictest/1234/")
+ self.assertEqual("Get 1234", res.data)
+
+ def test_basic_put(self):
+ res = self.client.put("/basictest/1234/")
+ self.assertEqual("Put 1234", res.data)
+
+ def test_basic_post(self):
+ res = self.client.post("/basictest/")
+ self.assertEqual("Post", res.data)
+
+ def test_basic_delete(self):
+ res = self.client.delete("/basictest/1234/")
+ self.assertEqual("Delete 1234", res.data)
+
+ def test_basic_method(self):
+ res = self.client.get("/basictest/other_method/")
+ self.assertEqual("Other Method", res.data)
+
+ def test_routed_method(self):
+ res = self.client.get("/basictest/another")
+ self.assertEqual("Another Method", res.data)
+
+ #.Make sure the automatic route wasn't generated
+ res = self.client.get("/basictest/another_method/")
+ self.assertNotEqual("Another Method", res.data)
+
+ def test_index_route_base(self):
+ res = self.client.get("/")
+ self.assertEqual("Home Page", res.data)
+
+ def tearDown(self):
+ pass
+
+if __name__ == "main":
+ unittest.main()

0 comments on commit aa4704b

Please sign in to comment.