diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc787be --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.pyc +*.pyo + +testapp/env/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ce6eea9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012, Igor Davydenko. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. 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. + + 3. Neither the name of tddspry nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..f3f73c1 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE +include MANIFEST.in +include README.rst diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..3995060 --- /dev/null +++ b/README.rst @@ -0,0 +1,161 @@ +=============== +Flask-LazyViews +=============== + +Registering url routes for your `Flask `_ app or +blueprint in lazy way :) + +**Based on original snippet from Flask documentation!** + +Requirements +============ + +* `Python `_ 2.6 or higher +* `Flask`_ 0.8 or higher + +Installation +============ + +:: + + $ pip install Flask-LazyViews + +License +======= + +``Flask-LazyViews`` is licensed under the `BSD License +`_. + +Usage +===== + +For application +--------------- + +``project/app.py`` + +:: + + from flask import Flask + from flask.ext.lazyviews import LazyViews + + + app = Flask(__name__) + views = LazyViews(app) + + views.add('/', 'views.home') + views.add('/page/', 'views.page') + +``project/views.py`` + +:: + + from flask import render_template + + + def home(): + return render_template('home.html') + + + def page(page_id): + page = get_page(page_id) + return render_template('page.html', page=page) + + +For blueprint +------------- + +.. note:: Blueprint name is ``test`` :) + +``project/app.py`` + +:: + + ... + + from project.test import blueprint as test_blueprint + + ... + + app.register_blueprint(test_blueprint, url_prefix='/test') + + +``project/test/__init__.py`` + +:: + + from flask import Blueprint + from flask.ext.lazyviews import LazyViews + + + blueprint = Blueprint('test', __name__) + views = LazyViews(blueprint, '.views') + + views.add('/', 'test') + views.add('/advanced', 'advanced_test', methods=('GET', 'POST')) + +``project/test/views.py`` + +:: + + from flask import render_template, request + + + def advanced_test(): + context = generate_context(request.form) + return render_template('test/advanced.html', **context) + + + def test(): + return render_template('test/test.html') + +Explanations +============ + +The main point of ``Flask-LazyViews`` is simplifying process of adding views +to the app and blueprint using `lazy technique +`_ from Flask +documentation. + +Also the next goal is simplifying ``viewname`` definition. For most cases our +views functions placed in ``.views`` module of app or blueprint, so we don't +need to input full path to that module. + +This especially useful for blueprints. Let see the example above, if we using +original snippet - we'll need to provide path to blueprint's views +module:: + + add_url(blueprint, '/', 'test.views.test') + +but with ``Flask-LazyViews`` we could to ignore ``test``. + +From other side if your view functions placed in some other location or you +need to provide full path to its - you still could do this. + +Also you could setup ``import_prefix`` like done in Django's ``patterns``:: + + views = LazyViews(app, 'views') + views.add('/', 'home') + views.add('/page/', 'page', methods=('GET', 'POST')) + +Important +--------- + +Be careful with ``import_prefix`` value if you used ``__name__`` as Flask +application name or blueprint ``import_name``. Setting relative path could +cause server errors. + +Bugs, feature requests? +======================= + +If you found some bug in ``Flask-LazyViews`` library, please, add new issue to +the project's `GitHub issues +`_. + +ChangeLog +========= + +0.1 +--- + +* Initial release. diff --git a/flask_lazyviews/__init__.py b/flask_lazyviews/__init__.py new file mode 100644 index 0000000..8e0b4a8 --- /dev/null +++ b/flask_lazyviews/__init__.py @@ -0,0 +1 @@ +from .lazyviews import LazyViews diff --git a/flask_lazyviews/lazyviews.py b/flask_lazyviews/lazyviews.py new file mode 100644 index 0000000..c6f1245 --- /dev/null +++ b/flask_lazyviews/lazyviews.py @@ -0,0 +1,35 @@ +from .utils import LazyView + + +class LazyViews(object): + """ + """ + def __init__(self, mixed, import_prefix=None): + """ + """ + self._mixed = mixed + + if import_prefix and import_prefix.startswith('.'): + import_name = \ + mixed.import_name if mixed.import_name != '__main__' else '' + + assert import_name, 'You should properly configure import name ' \ + 'for %r instance or edit import prefix to ' \ + 'not start with ".".' % \ + mixed.__class__.__name__ + + import_prefix = import_name + import_prefix + + self._import_prefix = import_prefix + + def add(self, url_rule, import_name, **options): + """ + """ + view = LazyView(self.build_import_name(import_name)) + self._mixed.add_url_rule(url_rule, view_func=view, **options) + + def build_import_name(self, import_name): + """ + """ + not_empty = lambda data: filter(lambda item: item, data) + return '.'.join(not_empty((self._import_prefix or None, import_name))) diff --git a/flask_lazyviews/utils.py b/flask_lazyviews/utils.py new file mode 100644 index 0000000..1d13e48 --- /dev/null +++ b/flask_lazyviews/utils.py @@ -0,0 +1,30 @@ +from werkzeug.utils import cached_property, import_string + + +__all__ = ('LazyView', ) + + +class LazyView(object): + """ + Import view function only when necessary. + """ + def __init__(self, name): + """ + Initialize ``LazyView`` instance for view that would be imported from + ``name`` path. + """ + self.__module__, self.__name__ = name.rsplit('.', 1) + self.import_name = name + + def __call__(self, *args, **kwargs): + """ + Make real call to the view. + """ + return self.view(*args, **kwargs) + + @cached_property + def view(self): + """ + Import view and cache it to current cls. + """ + return import_string(self.import_name) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2d74722 --- /dev/null +++ b/setup.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +import os + +from distutils.core import setup + + +DIRNAME = os.path.dirname(__file__) + +readme = open(os.path.join(DIRNAME, 'README.rst'), 'r') +README = readme.read() +readme.close() + + +setup( + name='Flask-LazyViews', + version='0.1', + description='Registering url routes for Flask app and blueprints in ' \ + 'lazy way.', + long_description=README, + author='Igor Davydenko', + author_email='playpauseandstop@gmail.com', + url='https://github.com/playpauseandstop/Flask-LazyViews', + install_requires=[ + 'Flask', + ], + packages=[ + 'flask_lazyviews', + ], + platforms='any', + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Web Environment', + 'Operating System :: OS Independent', + 'Topic :: Utilities', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'License :: OSI Approved :: BSD License', + ], + keywords='flask lazy views', + license='BSD License', +) diff --git a/testapp/Makefile b/testapp/Makefile new file mode 100644 index 0000000..a2c8311 --- /dev/null +++ b/testapp/Makefile @@ -0,0 +1,21 @@ +.PHONY: bootstrap server test + +# Project and environment settings +env = env +project = . +pip = $(env)/bin/pip +python = PYTHONPATH=.. $(env)/bin/python + +# Server settings +IP ?= 0.0.0.0 +PORT ?= 4354 + +bootstrap: + virtualenv --distribute $(env) + $(pip) install -r requirements.txt + +server: + $(python) $(project)/app.py $(IP):$(PORT) + +test: + $(python) $(project)/tests.py diff --git a/testapp/__init__.py b/testapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testapp/app.py b/testapp/app.py new file mode 100755 index 0000000..cdb2739 --- /dev/null +++ b/testapp/app.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python + +import os +import sys + +from flask import Flask +from flask.ext.lazyviews import LazyViews + +from testapp.test import blueprint as test_blueprint + + +# Init Flask application +app = Flask(__name__) + +# Add url routes to application +views = LazyViews(app) +views.add('/', 'views.home') +views.add('/page/', 'views.page', endpoint='flatpage') + +# Register test blueprint +app.register_blueprint(test_blueprint, url_prefix='/test') + + +if __name__ == '__main__': + host = '0.0.0.0' + port = 5000 + + if len(sys.argv) == 2: + mixed = sys.argv[1] + + try: + host, port = mixed.split(':') + except ValueError: + port = mixed + elif len(sys.argv) == 3: + host, port = sys.argv[1:] + + try: + port = int(port) + except (TypeError, ValueError): + print >> sys.stderr, 'Please, use proper digit value to the ' \ + '``port`` argument.\nCannot convert %r to ' \ + 'integer.' % port + + app.debug = bool(int(os.environ.get('DEBUG', 1))) + app.run(host=host, port=port) diff --git a/testapp/requirements.txt b/testapp/requirements.txt new file mode 100644 index 0000000..58f8785 --- /dev/null +++ b/testapp/requirements.txt @@ -0,0 +1,2 @@ +Flask>=0.8 +unittest2 diff --git a/testapp/templates/base.html b/testapp/templates/base.html new file mode 100644 index 0000000..5f8d1ea --- /dev/null +++ b/testapp/templates/base.html @@ -0,0 +1,21 @@ + + + + + Flask-LazyViews test project + + {% block extra_head %}{% endblock %} + + + +

Flask-LazyViews test project

+
{% block content %}{% endblock %}
+ {% block backlink %} + + {% endblock %} + {% block extra_body %}{% endblock %} + + + diff --git a/testapp/templates/home.html b/testapp/templates/home.html new file mode 100644 index 0000000..9d3c82c --- /dev/null +++ b/testapp/templates/home.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% block content %} + +

Home page

+ + + +{% endblock %} +{% block backlink %}{% endblock %} diff --git a/testapp/templates/page.html b/testapp/templates/page.html new file mode 100644 index 0000000..bced293 --- /dev/null +++ b/testapp/templates/page.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} +{% block content %} + +

Page #{{ page }}

+

Dummy page content ;)

+ +{% endblock %} diff --git a/testapp/test/__init__.py b/testapp/test/__init__.py new file mode 100644 index 0000000..128b44b --- /dev/null +++ b/testapp/test/__init__.py @@ -0,0 +1 @@ +from .blueprint import blueprint diff --git a/testapp/test/blueprint.py b/testapp/test/blueprint.py new file mode 100644 index 0000000..bf12ee0 --- /dev/null +++ b/testapp/test/blueprint.py @@ -0,0 +1,9 @@ +from flask import Blueprint +from flask.ext.lazyviews import LazyViews + + +blueprint = Blueprint('test', 'testapp.test', template_folder='templates') + +views = LazyViews(blueprint, '.views') +views.add('/', 'test') +views.add('/advanced', 'advanced', methods=('GET', 'POST')) diff --git a/testapp/test/templates/test/advanced.html b/testapp/test/templates/test/advanced.html new file mode 100644 index 0000000..5dc2d84 --- /dev/null +++ b/testapp/test/templates/test/advanced.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% block content %} + +

Advanced test page

+ +

$REQUEST

+
{{ values }}
+ +

Make POST request

+
+

+ +

+

+ +

+
+ +{% endblock %} diff --git a/testapp/test/templates/test/test.html b/testapp/test/templates/test/test.html new file mode 100644 index 0000000..64db708 --- /dev/null +++ b/testapp/test/templates/test/test.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} +{% block content %} +

Test page

+ +{% endblock %} diff --git a/testapp/test/utils.py b/testapp/test/utils.py new file mode 100644 index 0000000..8e912d8 --- /dev/null +++ b/testapp/test/utils.py @@ -0,0 +1,2 @@ +def compute_context(values): + return values.to_dict() diff --git a/testapp/test/views.py b/testapp/test/views.py new file mode 100644 index 0000000..9412dc6 --- /dev/null +++ b/testapp/test/views.py @@ -0,0 +1,9 @@ +from flask import render_template, request + + +def advanced(): + return render_template('test/advanced.html', values=request.values) + + +def test(): + return render_template('test/test.html') diff --git a/testapp/tests.py b/testapp/tests.py new file mode 100755 index 0000000..8dcc6b9 --- /dev/null +++ b/testapp/tests.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python + +import unittest + +# Simple manipulation to use ``unittest2`` if current Python version is +# less than 2.7 +if not hasattr(unittest.TestCase, 'assertIn'): + import unittest2 as unittest + +from random import choice, randint +from string import letters + +from flask import Blueprint, Flask, url_for +from flask.ext.lazyviews import LazyViews +from werkzeug.utils import ImportStringError + +from app import app +from test import blueprint + + +class TestFlaskLazyViews(unittest.TestCase): + + def setUp(self): + app.config['TESTING'] = True + self.app = app.test_client() + + def tearDown(self): + app.config['TESTING'] = False + + def url(self, *args, **kwargs): + with app.test_request_context(): + return url_for(*args, **kwargs) + + def test_app(self): + page = randint(1, 9999) + + home_url = self.url('home') + page_url = self.url('flatpage', page_id=page) + first_page_url = self.url('flatpage', page_id=1) + + response = self.app.get(home_url) + self.assertEqual(response.status_code, 200) + self.assertIn('

Flask-LazyViews test project

', response.data) + self.assertIn('' % first_page_url, response.data) + + response = self.app.get(page_url) + self.assertEqual(response.status_code, 200) + self.assertIn('Page #%d' % page, response.data) + self.assertIn('' % home_url, response.data) + + def test_blueprint(self): + home_url = self.url('home') + test_url = self.url('test.test') + advanced_url = self.url('test.advanced') + + response = self.app.get(home_url) + self.assertEqual(response.status_code, 200) + self.assertIn('' % test_url, response.data) + self.assertIn('To advanced test page' % advanced_url, + response.data + ) + + number = str(randint(1, 9999)) + + response = self.app.get('%s?q=%s' % (advanced_url, number)) + self.assertEqual(response.status_code, 200) + self.assertIn('Advanced test page', response.data) + self.assertIn('$REQUEST', response.data) + self.assertIn(number, response.data) + self.assertIn('Make POST request', response.data) + + text = ''.join([choice(letters) for i in range(16)]) + + response = self.app.post(advanced_url, data={'text': text}) + self.assertEqual(response.status_code, 200) + self.assertIn(text, response.data) + + def test_custom_config_app(self): + views = LazyViews(app, 'testapp.views') + views.add('/default-page', + 'page', + defaults={'page_id': 1}, + endpoint='default_page') + + self.assertIn('default_page', app.view_functions) + + test_app = app.test_client() + response = test_app.get(self.url('default_page')) + self.assertEqual(response.status_code, 200) + self.assertIn('Page #1', response.data) + + def test_custom_config_blueprint(self): + views = LazyViews(blueprint, blueprint.import_name) + views.add('/more-advanced', + 'views.advanced', + endpoint='more_advanced', + methods=('GET', 'POST', 'PUT')) + + # Don't forget to re-register blueprint + app.blueprints.pop('test') + app.register_blueprint(blueprint, url_prefix='/test') + + self.assertIn('test.more_advanced', app.view_functions) + + test_app = app.test_client() + response = test_app.put(self.url('test.more_advanced')) + self.assertEqual(response.status_code, 200) + self.assertIn('Advanced test page', response.data) + + def test_error_config_app(self): + views = LazyViews(app, 'weird.path') + views.add('/default-page', + 'views.page', + defaults={'page_id': 1}, + endpoint='default_page') + + self.assertIn('default_page', app.view_functions) + + test_app = app.test_client() + self.assertRaises(ImportStringError, + test_app.get, + self.url('default_page')) + + views = LazyViews(app, 'testapp.views') + views.add('/another-default-page', + 'does_not_exist', + endpoint='another_default_page') + + self.assertIn('another_default_page', app.view_functions) + + test_app = app.test_client() + self.assertRaises(ImportStringError, + test_app.get, + self.url('another_default_page')) + + def test_error_config_blueprint(self): + views = LazyViews(blueprint, 'weird.path') + views.add('/more-advanced', + 'views.advanced', + endpoint='more_advanced') + + app.blueprints.pop('test') + app.register_blueprint(blueprint, url_prefix='/test') + + self.assertIn('test.more_advanced', app.view_functions) + + test_app = app.test_client() + self.assertRaises(ImportStringError, + test_app.get, + self.url('test.more_advanced')) + + views = LazyViews(blueprint, blueprint.import_name) + views.add('/more-more-advanced', + 'views.does_not_exist', + endpoint='more_more_advanced') + + app.blueprints.pop('test') + app.register_blueprint(blueprint, url_prefix='/test') + + self.assertIn('test.more_more_advanced', app.view_functions) + + test_app = app.test_client() + self.assertRaises(ImportStringError, + test_app.get, + self.url('test.more_more_advanced')) + + +if __name__ == '__main__': + unittest.main() diff --git a/testapp/views.py b/testapp/views.py new file mode 100644 index 0000000..6d463a7 --- /dev/null +++ b/testapp/views.py @@ -0,0 +1,14 @@ +from random import choice +from string import digits, letters + +from flask import render_template + + +def home(): + query = u''.join([choice(letters + digits) for i in range(16)]) + return render_template('home.html', query=query) + + +def page(page_id): + page = int(page_id) + return render_template('page.html', page=page)