Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

versin 1.2.0

  • Loading branch information...
commit 3673f760bddf34ecb02d8a31f7e0af6612b0f60f 1 parent 2458e7e
@jpscaletti jpscaletti authored
Showing with 1,431 additions and 1,375 deletions.
  1. +1 −1  AUTHORS.md
  2. +27 −0 CHANGES.md
  3. +1 −1  LEGAL.md
  4. +9 −6 README.md
  5. +10 −8 README.rst
  6. +6 −5 requirements.txt
  7. +11 −10 shake/__init__.py
  8. +94 −69 shake/app.py
  9. +0 −483 shake/babel.py
  10. +37 −22 shake/cli/__init__.py
  11. +0 −65 shake/cli/globals.py
  12. +7 −40 shake/cli/helpers.py
  13. +6 −8 shake/config.py
  14. +0 −89 shake/controllers.py
  15. 0  shake/{default_views → default_templates}/error.html
  16. 0  shake/{default_views → default_templates}/error_notallowed.html
  17. 0  shake/{default_views → default_templates}/error_notfound.html
  18. 0  shake/{default_views → default_templates}/shake_default_view.css
  19. +1 −1  shake/helpers.py
  20. +513 −0 shake/i18n.py
  21. +171 −0 shake/render.py
  22. +1 −1  shake/session.py
  23. +11 −11 shake/skeletons/project/README.md
  24. +2 −2 shake/skeletons/project/bundles/common/__init__.py
  25. +0 −26 shake/skeletons/project/bundles/common/controllers.py
  26. +26 −0 shake/skeletons/project/bundles/common/views.py
  27. +9 −13 shake/skeletons/project/bundles/users/__init__.py
  28. +2 −2 shake/skeletons/project/bundles/users/manage.py
  29. 0  shake/skeletons/project/{views/users/.gitinclude → locales/en.yml}
  30. +4 −5 shake/skeletons/project/main.py
  31. +1 −1  shake/skeletons/project/manage.py
  32. +4 −1 shake/skeletons/project/settings/base.py.tmpl
  33. +3 −3 shake/skeletons/project/settings/production.py
  34. +2 −2 shake/skeletons/project/settings/req.txt
  35. +1 −1  shake/skeletons/project/{views → templates}/common/base.html
  36. 0  shake/skeletons/project/{views → templates}/common/base_error.html
  37. 0  shake/skeletons/project/{views → templates}/common/error.html
  38. 0  shake/skeletons/project/{views → templates}/common/error_notallowed.html
  39. 0  shake/skeletons/project/{views → templates}/common/error_notfound.html
  40. +1 −1  shake/skeletons/project/{views → templates}/index.html
  41. 0  shake/skeletons/project/templates/users/.gitinclude
  42. +2 −2 shake/skeletons/resource/bundle/__init__.py.tmpl
  43. +6 −6 shake/skeletons/resource/bundle/{controllers.py.tmpl → views.py.tmpl}
  44. 0  shake/skeletons/resource/{views → templates}/edit.html.tmpl
  45. 0  shake/skeletons/resource/{views → templates}/flash.html
  46. 0  shake/skeletons/resource/{views → templates}/index.html.tmpl
  47. 0  shake/skeletons/resource/{views → templates}/show.html.tmpl
  48. +4 −3 shake/templates.py
  49. +69 −139 shake/views.py
  50. +84 −8 shake/wrappers.py
  51. +0 −1  tests/res/en-US.yml
  52. +0 −1  tests/res/en.yml
  53. +0 −1  tests/res/es.yml
  54. 0  tests/res/{view.html → tmpl.html}
  55. 0  tests/res/{view.txt → tmpl.txt}
  56. +42 −42 tests/test_app.py
  57. +0 −104 tests/test_controllers.py
  58. +13 −12 tests/test_helpers.py
  59. +174 −0 tests/test_render.py
  60. +76 −179 tests/test_views.py
View
2  AUTHORS.md
@@ -1,6 +1,6 @@
# Authors
-Shake is written and maintained by the Lúcuma labs team:
+Shake is written and maintained by the Lúcuma team:
Project Leader / Developer:
View
27 CHANGES.md
@@ -0,0 +1,27 @@
+# Changes
+
+
+## Version 1.2.0
+
+- New Rails-like internationalization framework. Available at `app.i18n` or as {{ t('key_name', **kwargs) }} in the templates.
+
+- Auto-created `Render` instance at `app.render`. Uses the `'<app-path>/templates/'` folder by default.
+
+- A static folder is added by default at `'/static/ -> <app-path>/static/'` if `DEBUG=True` and no other static paths are defined.
+
+- `Shake` must be now instantiated with ``__file__'` as the first argument. Example:
+
+ app = Shake(__file__, settings)
+
+ this is used to calculate the templates, locales and static folders.
+
+- The environment setting is now loaded from the `.SHAKE_ENV` file instead from the `manage.py` script (it was unreliable for multiple `manage.py` scripts). If the files is not found, `Shake.get_env` return `'development'` by default. `Shake.set_env` create that file if it doesn't exists or overwrites it.
+
+- The `manage.py` in the default project now includes `Solution`'s fixtures related functions.
+
+- The word `'templates'` is now used instead of `'views'`, and `'views'` is now used instead of `'controllers'`.
+
+- Added a new template global function: `link_to` makes easier to create HTML anchor element for navigation, that are marked as "active" when visiting a particular URL.
+
+- General refactoring of the code.
+
View
2  LEGAL.md
@@ -1,6 +1,6 @@
# Legal
-© 2010 by Lúcuma labs (http://lucumalabs.com).
+© 2010 by Lúcuma (http://lucumalabs.com).
MIT License. (http://www.opensource.org/licenses/mit-license.php)
View
15 README.md
@@ -1,24 +1,27 @@
# Shake
-A web framework mixed from the best ingredients (Werkzeug, Jinja and maybe SQLAlchemy, babel, etc.)
+A web framework mixed from the best ingredients (Werkzeug, Jinja2 and maybe SQLAlchemy, Babel, etc.)
+
+It can be minimal like this::
```python
from shake import Shake
-app = Shake()
+app = Shake(__file__)
+app.route('/', hello)
def hello(request):
- return 'Hello World!'
-
-app.add_url('/', hello)
+ return 'Hello World!'
if __name__ == "__main__":
app.run()
```
+Or a full featured (yet configurable if needed) framework.
+
---------------------------------------
-© 2010 by [Lúcuma labs] (http://lucumalabs.com).
+© 2010 by [Lúcuma] (http://lucumalabs.com).
See `AUTHORS.md` for more details.
License: [MIT License] (http://www.opensource.org/licenses/mit-license.php).
View
18 README.rst
@@ -1,18 +1,20 @@
-========
+================
Shake
-========
+================
-A web framework mixed from the best ingredients (Werkzeug, Jinja and maybe SQLAlchemy, babel, etc.)
+A web framework mixed from the best ingredients (Werkzeug, Jinja2 and maybe SQLAlchemy, Babel, etc.)
+
+It can be minimal like this::
-::
from shake import Shake
- app = Shake()
+ app = Shake(__file__)
+ app.route('/', hello)
def hello(request):
- return 'Hello World!'
-
- app.add_url('/', hello)
+ return 'Hello World!'
if __name__ == "__main__":
app.run()
+
+Or a full featured (yet configurable if needed) framework.
View
11 requirements.txt
@@ -4,9 +4,10 @@
#
# pip install -r requirements.txt
-Babel
-Jinja2>=2.4
-pyCEO>=1.0.3
+babel
+inflector
+jinja2>=2.4
+pyceo>=1.0.3
pytz
-Voodoo>=0.7
-Werkzeug>=0.7
+voodoo>=0.7
+werkzeug>=0.7
View
21 shake/__init__.py
@@ -1,35 +1,37 @@
# -*- coding: utf-8 -*-
"""
+ --------------------------
Shake
--------------------------
- A web framework mixed from the best ingredients:
+ A web framework mixed from the best ingredients.
+ It can be minimal like this:
from shake import Shake
- app = Shake()
+ app = Shake(__file__)
+ app.route('/', hello)
def hello(request):
- return 'Hello World!'
-
- app.add_url('/', hello)
+ return 'Hello World!'
if __name__ == "__main__":
app.run()
+ Or a full featured (yet configurable if you need it) framework.
---------------------------------------
- © 2010 by [Lúcuma labs] (http://lucumalabs.com).
+ © 2010 by [Lúcuma] (http://lucumalabs.com).
See `AUTHORS.md` for more details.
License: [MIT License] (http://www.opensource.org/licenses/mit-license.php).
Portions of code and/or inspiration taken from:
- * Flask <flask.pocoo.org> Copyright 2010, Armin Ronacher.
* Werkzeug <werkzeug.pocoo.org> Copyright 2010, the Werkzeug Team.
+ * Flask <flask.pocoo.org> Copyright 2010, Armin Ronacher.
Used under the modified BSD license. See LEGAL.md for more details
"""
-# Utilities we import from Jinja and Werkzeug that are unused
+# Utilities we import from Werkzeug that are unused
# in the module but are exported as public interface.
from jinja2.exceptions import TemplateNotFound
from werkzeug.exceptions import *
@@ -37,8 +39,8 @@ def hello(request):
from werkzeug.utils import cached_property, import_string, redirect
from .app import *
-from .controllers import *
from .helpers import *
+from .render import *
from .routes import *
from .serializers import json
from .session import *
@@ -49,7 +51,6 @@ def hello(request):
## Aliases
NotAllowed = Forbidden
-ViewNotFound = TemplateNotFound
redirect_to = redirect
__version__ = '1.2.0'
View
163 shake/app.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
"""
- # Shake.app
+ Shake.app
+ --------------------------
This module implements the central WSGI application object.
@@ -8,6 +9,7 @@
from datetime import datetime, timedelta
import io
import os
+from os.path import isdir, dirname, join, abspath, normpath, realpath
import socket
from pyceo import Manager
@@ -15,13 +17,13 @@
from werkzeug.local import LocalManager
from werkzeug.serving import run_simple
from werkzeug.utils import import_string
-from werkzeug.wrappers import BaseResponse
from .config import get_settings_object
from .helpers import local, to_unicode
+from .i18n import I18n, LOCALES_DIR
+from .render import Render, TEMPLATES_DIR
from .routes import Map, Rule
-from .serializers import to_json
-from .wrappers import Request, Response
+from .wrappers import Request, Response, make_response
__all__ = (
@@ -50,14 +52,17 @@ class Shake(object):
"""Implements a WSGI application and acts as the central
object.
+ root_path
+ : the root path of the application. The `locales` and `templates` dirs
+ will be based on this value.
settings
- : A module or dict with the custom settings.
+ : a module or dict with the custom settings.
Usually you create a `Shake` instance in your main module or
in the `__init__.py` file of your package like this:
from shake import Shake
- app = Shake(settings)
+ app = Shake(__file__, settings)
"""
@@ -66,51 +71,44 @@ class Shake(object):
# The class that is used for response objects.
response_class = Response
+
- def __init__(self, *args):
- url_map = []
- settings = {}
- largs = len(args)
- if largs == 1:
- settings = args[0]
- if isinstance(args[0], (list, tuple, Map)):
- url_map = args[0]
- settings = {}
- elif largs > 1:
- url_map = args[0]
- settings = args[1]
-
+ def __init__(self, root_path=None, settings=None):
# Functions to run before each request and response
self.before_request_funcs = []
# Functions to run before each response
self.after_request_funcs = []
# Functions to run if an exception occurs
self.on_exception_funcs = []
-
# A dict of static `url, path` pairs to be used during development.
self.static_dirs = {}
- settings = get_settings_object(settings)
+ root_path = root_path or '.'
+ root_path = normpath(abspath(realpath(root_path)))
+ # Instead of a path, we've probably recieved the value of __file__
+ if not isdir(root_path):
+ root_path = dirname(root_path)
+ self.root_path = root_path
+
+ settings = get_settings_object(settings or {})
self.settings = settings
- if not isinstance(url_map, Map):
- url_map = Map(url_map, default_subdomain=settings.DEFAULT_SUBDOMAIN)
- self.url_map = url_map
-
+ self.assert_secret_key()
+
+ self.url_map = Map([], default_subdomain=settings.DEFAULT_SUBDOMAIN)
self.error_handlers = {
403: settings.PAGE_NOT_ALLOWED,
404: settings.PAGE_NOT_FOUND,
500: settings.PAGE_ERROR,
}
-
self.request_class.max_content_length = settings.MAX_CONTENT_LENGTH
self.request_class.max_form_memory_size = settings.MAX_FORM_MEMORY_SIZE
-
- self.assert_secret_key()
self.session_expires = timedelta(hours=settings.SESSION_EXPIRES)
+ self.create_default_services()
+
def assert_secret_key(self):
- """Make sure the SECRET_KEY (if there's one defined in the settings)
- is long enough.
+ """Make sure the SECRET_KEY is long enough (only if there's one
+ defined in the settings).
"""
key = self.settings.SECRET_KEY
@@ -118,6 +116,24 @@ def assert_secret_key(self):
raise RuntimeError("Your 'SECRET_KEY' setting is too short to be"
" safe. Make sure is *at least* %i chars long."
% SECRET_KEY_MINLEN)
+
+
+ def create_default_services(self):
+ """Creates the default `render` and `i18n` services using the
+ `root_path` as the base path for the `'templates'` and `'locales'` dirs.
+
+ """
+ locales_dir = (self.settings.get('LOCALES_DIR') or
+ join(self.root_path, LOCALES_DIR))
+ if isinstance(locales_dir, basestring):
+ locales_dir = [locales_dir]
+ self.i18n = I18n(locales_dir, app=self)
+
+ templates_dir = join(self.root_path, TEMPLATES_DIR)
+ self.render = Render(templates_dir, i18n=self.i18n,
+ default_mimetype=self.settings.get('DEFAULT_MIMETYPE'),
+ response_class=self.response_class)
+
def route(self, url, *args, **kwargs):
"""A decorator for mounting an endpoint in a URL.
@@ -132,11 +148,13 @@ def real_decorator(target):
self.url_map.add(Rule(url, target, *args, **kwargs))
return target
return real_decorator
+
def add_url(self, rule, *args, **kwargs):
"""Adds an URL rule.
Example:
+
def example():
return 'example'
@@ -144,6 +162,7 @@ def example():
"""
self.url_map.add(Rule(rule, *args, **kwargs))
+
def add_urls(self, urls):
"""Adds all the URL rules from a list (or iterable).
@@ -151,6 +170,7 @@ def add_urls(self, urls):
"""
for url in urls:
self.url_map.add(url)
+
def add_static(self, url, path):
"""Can be used to specify an URL for static files on the web and
@@ -160,11 +180,12 @@ def add_static(self, url, path):
"""
url = '/' + url.strip('/')
- path = os.path.normpath(os.path.realpath(path))
+ path = normpath(abspath(realpath(path)))
# Instead of a path, we've probably recieved the value of __file__
- if os.path.isfile(path):
- path = os.path.join(os.path.dirname(path), STATIC_DIR)
+ if not isdir(path):
+ path = join(dirname(path), STATIC_DIR)
self.static_dirs[url] = path
+
def before_request(self, function):
"""Register a function to run before each request.
@@ -174,6 +195,7 @@ def before_request(self, function):
if function not in self.before_request_funcs:
self.before_request_funcs.append(function)
return function
+
def after_request(self, function):
"""Register a function to be run after each request.
@@ -185,6 +207,10 @@ def after_request(self, function):
if function not in self.after_request_funcs:
self.after_request_funcs.append(function)
return function
+
+ # Backwards compatibilty. Will go away in v1.3
+ before_response = after_request
+
def on_exception(self, function):
"""Register a function to be run if an exception
@@ -194,17 +220,20 @@ def on_exception(self, function):
if function not in self.on_exception_funcs:
self.on_exception_funcs.append(function)
return function
+
def preprocess_request(self, request):
for handler in self.before_request_funcs:
resp_value = handler(request)
if resp_value is not None:
return resp_value
+
def process_response(self, response):
for handler in self.after_request_funcs:
response = handler(response)
return response
+
def save_session(self, session, response):
"""Saves the session if it needs updates. For the default
@@ -220,6 +249,7 @@ def save_session(self, session, response):
session_data = session.serialize()
response.set_cookie(self.settings.SESSION_COOKIE_NAME,
session_data, httponly=True, expires=expires)
+
def wsgi_app(self, environ, start_response):
"""The actual WSGI application. This is not implemented in
@@ -253,6 +283,7 @@ def wsgi_app(self, environ, start_response):
local_manager.cleanup()
return response(environ, start_response)
+
def force_script_name(self, environ):
"""In some servers (like Lighttpd), when deploying using FastCGI
and you want the application to work in the URL root you have to work
@@ -270,14 +301,15 @@ def force_script_name(self, environ):
environ['REDIRECT_URI'] = redirect_uri.replace(
script_name, new_script_name)
+
def dispatch(self, request):
"""Does the request dispatching. Matches the URL and returns the
- return value of the controller or error handler. This does not have to
+ 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 `make_response`.
If DEBUG=True, a `NotFound` exception emitted by your code is treated
- like a regular exception without error handlers, so iy's easy
+ like a regular exception without error handlers, so it's easy
to differetiate it from a real `HTTP 404: NOT FOUND`.
"""
@@ -299,6 +331,7 @@ def dispatch(self, request):
return response
+
def match_url(self, request):
local.urls = urls = self.create_url_adapter(request)
rule, kwargs = urls.match(return_rule=True)
@@ -311,16 +344,21 @@ def match_url(self, request):
request.kwargs = kwargs
return endpoint, kwargs
+
def create_url_adapter(self, request):
"""Creates a URL adapter for the given request.
"""
- return self.url_map.bind_to_environ(request.environ,
- server_name=self.settings.SERVER_NAME)
+ server_name = self.settings.SERVER_NAME
+ port = self.settings.SERVER_PORT
+ if port:
+ server_name = '%s:%s' % (server_name, port)
+ return self.url_map.bind_to_environ(request, server_name=server_name)
+
def make_response(self, resp='', status=None, headers=None, **kwargs):
"""Converts the return value from a view function to a real
- response object that is an instance of `response_class`.
+ response object that is an instance of `Shake.response_class`.
The following types are allowed for `resp_value`:
@@ -343,7 +381,7 @@ def make_response(self, resp='', status=None, headers=None, **kwargs):
Parameters:
resp_value
- : the return value from the controller function.
+ : the return value from the view function.
status
: An optional status code.
headers
@@ -352,35 +390,9 @@ def make_response(self, resp='', status=None, headers=None, **kwargs):
return: an instance of `response_class`
"""
- if resp is None:
- resp = ''
-
- if isinstance(resp, dict):
- kwargs['mimetype'] = 'application/json'
- resp = to_json(resp, indent=None)
-
- if not isinstance(resp, BaseResponse):
- if isinstance(resp, basestring):
- resp = self.response_class(resp, status=status, headers=headers,
- **kwargs)
- headers = status = None
- elif not callable(resp):
- resp = to_unicode(resp)
- resp = self.response_class(resp, status=status, headers=headers,
- **kwargs)
- headers = status = None
- else:
- resp = self.response_class.force_type(resp, local.request.environ)
+ return make_response(resp, status=status, headers=headers,
+ response_class=self.response_class, **kwargs)
- if status is not None:
- if isinstance(status, basestring):
- resp.status = status
- else:
- resp.status_code = status
- if headers:
- resp.headers.extend(headers)
-
- return resp
def handle_http_exception(self, request, exception):
"""Handles an HTTP exception. By default try to use the handler
@@ -408,6 +420,7 @@ def handle_http_exception(self, request, exception):
response = self.make_response(resp_value, status)
return response
+
def handle_exception(self, request, error):
"""Default exception handling that kicks in when an exception
occours that is not caught. In debug mode the exception is
@@ -427,6 +440,7 @@ def handle_exception(self, request, error):
response = self.make_response(resp_value, 500)
return response
+
def print_welcome_msg(self):
"""Prints a welcome message, if you run this application
without declaring URLs first.
@@ -441,6 +455,7 @@ def print_welcome_msg(self):
'-' * wml,
''])
+
def print_help_msg(self, host, port):
"""Prints a help message.
@@ -454,6 +469,7 @@ def print_help_msg(self, host, port):
if ips:
print ' * Running on http://%s:%s' % (ips[0], port)
print '-- Quit the server with Ctrl+C --'
+
def run(self, host=None, port=None, debug=None, reloader=None,
reloader_interval=2, threaded=True, processes=1,
@@ -484,6 +500,9 @@ def run(self, host=None, port=None, debug=None, reloader=None,
: an SSL context for the connection. Either an OpenSSL context, the
string 'adhoc' if the server should automatically create one, or
`None` to disable SSL (which is the default).
+
+ If no static_dir was defined (using `add_static`) a default`'/static'`
+ URL mounted at ``<root_path>/static'` is added automatically.
"""
host = host or self.settings.SERVER_NAME
@@ -492,7 +511,12 @@ def run(self, host=None, port=None, debug=None, reloader=None,
self.settings.DEBUG)
reloader = bool(reloader if (reloader is not None) else
self.settings.RELOADER)
-
+ static_dirs = self.static_dirs
+
+ if not static_dirs:
+ static_path = join(self.root_path, STATIC_DIR)
+ static_dirs['/static'] = static_path
+
self.print_welcome_msg()
self.print_help_msg(host, port)
@@ -503,7 +527,7 @@ def run(self, host=None, port=None, debug=None, reloader=None,
threaded=threaded,
processes=processes,
ssl_context=ssl_context,
- static_files=self.static_dirs,
+ static_files=static_dirs,
**kwargs)
def test_client(self):
@@ -516,6 +540,7 @@ def test_client(self):
if self.settings.SERVER_NAME == '127.0.0.1':
self.settings.SERVER_NAME = 'localhost'
return Client(self, self.response_class, use_cookies=True)
+
def __call__(self, environ, start_response):
"""Shortcut for `wsgi_app`.
View
483 shake/babel.py
@@ -1,483 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
- Shake.babel
- --------------------------
-
- Implements i18n/l10n support for Shake applications based on Babel.
-
- ----------------
- Adapted from Flask-Babel <http://packages.python.org/Flask-Babel/>
- (c) 2010 by Armin Ronacher.
- used under the modified BSD license.
-
-"""
-from __future__ import absolute_import
-import os
-
-# this is a workaround for a snow leopard bug that babel does not
-# work around :)
-if os.environ.get('LC_CTYPE', '').lower() == 'utf-8':
- os.environ['LC_CTYPE'] = 'en_US.utf-8'
-
-from datetime import datetime
-
-from babel import dates, numbers, support, Locale
-from pytz import timezone, UTC
-from werkzeug import ImmutableDict
-
-
-class Babel(object):
- """Central controller class that can be used to configure how
- Shake.babel behaves.
- """
-
- default_date_formats = ImmutableDict({
- 'time': 'medium',
- 'date': 'medium',
- 'datetime': 'medium',
- 'time.short': None,
- 'time.medium': None,
- 'time.full': None,
- 'time.long': None,
- 'date.short': None,
- 'date.medium': None,
- 'date.full': None,
- 'date.long': None,
- 'datetime.short': None,
- 'datetime.medium': None,
- 'datetime.full': None,
- 'datetime.long': None,
- })
-
- def __init__(self, app=None, default_locale='en', default_timezone='UTC',
- date_formats=None):
- self._default_locale = default_locale
- self._default_timezone = default_timezone
- self.date_formats = self.default_date_formats.copy()
- self.app = app
-
- #: a mapping of Babel datetime format strings that can be modified
- #: to change the defaults. If you invoke :func:`format_datetime`
- #: and do not provide any format string Flask-Babel will do the
- #: following things:
- #:
- #: 1. look up ``date_formats['datetime']``. By default ``'medium'``
- #: is returned to enforce medium length datetime formats.
- #: 2. ``date_formats['datetime.medium'] (if ``'medium'`` was
- #: returned in step one) is looked up. If the return value
- #: is anything but `None` this is used as new format string.
- #: otherwise the default for that language is used.
- self.locale_selector_func = None
- self.timezone_selector_func = None
-
- def localeselector(self, f):
- """Registers a callback function for locale selection. The default
- behaves as if a function was registered that returns `None` all the
- time. If `None` is returned, the locale falls back to the one from
- the configuration.
-
- This has to return the locale as string (eg: ``'de_AT'``, ''`en_US`'')
- """
- assert self.locale_selector_func is None, \
- 'a localeselector function is already registered'
- self.locale_selector_func = f
- return f
-
- def timezoneselector(self, f):
- """Registers a callback function for timezone selection. The default
- behaves as if a function was registered that returns `None` all the
- time. If `None` is returned, the timezone falls back to the one from
- the configuration.
-
- This has to return the timezone as string (eg: ``'Europe/Vienna'``)
- """
- assert self.timezone_selector_func is None, \
- 'a timezoneselector function is already registered'
- self.timezone_selector_func = f
- return f
-
-
- def list_translations(self):
- """Returns a list of all the locales translations exist for. The
- list returned will be filled with actual locale objects and not just
- strings.
-
- .. versionadded:: 0.6
- """
- dirname = os.path.join(self.app.root_path, 'translations')
- if not os.path.isdir(dirname):
- return []
- result = []
- for folder in os.listdir(dirname):
- locale_dir = os.path.join(dirname, folder, 'LC_MESSAGES')
- if not os.path.isdir(locale_dir):
- continue
- if filter(lambda x: x.endswith('.mo'), os.listdir(locale_dir)):
- result.append(Locale.parse(folder))
- if not result:
- result.append(Locale.parse(self._default_locale))
- return result
-
- @property
- def default_locale(self):
- """The default locale from the configuration as instance of a
- `babel.Locale` object.
- """
- return Locale.parse(self.app.config['BABEL_DEFAULT_LOCALE'])
-
- @property
- def default_timezone(self):
- """The default timezone from the configuration as instance of a
- `pytz.timezone` object.
- """
- return timezone(self.app.config['BABEL_DEFAULT_TIMEZONE'])
-
-
-def get_translations():
- """Returns the correct gettext translations that should be used for
- this request. This will never fail and return a dummy translation
- object if used outside of the request or if a translation cannot be
- found.
- """
- ctx = _request_ctx_stack.top
- if ctx is None:
- return None
- translations = getattr(ctx, 'babel_translations', None)
- if translations is None:
- dirname = os.path.join(ctx.app.root_path, 'translations')
- translations = support.Translations.load(dirname, [get_locale()])
- ctx.babel_translations = translations
- return translations
-
-
-def get_locale():
- """Returns the locale that should be used for this request as
- `babel.Locale` object. This returns `None` if used outside of
- a request.
- """
- ctx = _request_ctx_stack.top
- if ctx is None:
- return None
- locale = getattr(ctx, 'babel_locale', None)
- if locale is None:
- babel = ctx.app.extensions['babel']
- if babel.locale_selector_func is None:
- locale = babel.default_locale
- else:
- rv = babel.locale_selector_func()
- if rv is None:
- locale = babel.default_locale
- else:
- locale = Locale.parse(rv)
- ctx.babel_locale = locale
- return locale
-
-
-def get_timezone():
- """Returns the timezone that should be used for this request as
- `pytz.timezone` object. This returns `None` if used outside of
- a request.
- """
- ctx = _request_ctx_stack.top
- tzinfo = getattr(ctx, 'babel_tzinfo', None)
- if tzinfo is None:
- babel = ctx.app.extensions['babel']
- if babel.timezone_selector_func is None:
- tzinfo = babel.default_timezone
- else:
- rv = babel.timezone_selector_func()
- if rv is None:
- tzinfo = babel.default_timezone
- else:
- if isinstance(rv, basestring):
- tzinfo = timezone(rv)
- else:
- tzinfo = rv
- ctx.babel_tzinfo = tzinfo
- return tzinfo
-
-
-def refresh():
- """Refreshes the cached timezones and locale information. This can
- be used to switch a translation between a request and if you want
- the changes to take place immediately, not just with the next request::
-
- user.timezone = request.form['timezone']
- user.locale = request.form['locale']
- refresh()
- flash(gettext('Language was changed'))
-
- Without that refresh, the :func:`~flask.flash` function would probably
- return English text and a now German page.
- """
- ctx = _request_ctx_stack.top
- for key in 'babel_locale', 'babel_tzinfo', 'babel_translations':
- if hasattr(ctx, key):
- delattr(ctx, key)
-
-
-def _get_format(key, format):
- """A small helper for the datetime formatting functions. Looks up
- format defaults for different kinds.
- """
- babel = _request_ctx_stack.top.app.extensions['babel']
- if format is None:
- format = babel.date_formats[key]
- if format in ('short', 'medium', 'full', 'long'):
- rv = babel.date_formats['%s.%s' % (key, format)]
- if rv is not None:
- format = rv
- return format
-
-
-def to_user_timezone(datetime):
- """Convert a datetime object to the user's timezone. This automatically
- happens on all date formatting unless rebasing is disabled. If you need
- to convert a :class:`datetime.datetime` object at any time to the user's
- timezone (as returned by :func:`get_timezone` this function can be used).
- """
- if datetime.tzinfo is None:
- datetime = datetime.replace(tzinfo=UTC)
- tzinfo = get_timezone()
- return tzinfo.normalize(datetime.astimezone(tzinfo))
-
-
-def to_utc(datetime):
- """Convert a datetime object to UTC and drop tzinfo. This is the
- opposite operation to :func:`to_user_timezone`.
- """
- if datetime.tzinfo is None:
- datetime = get_timezone().localize(datetime)
- return datetime.astimezone(UTC).replace(tzinfo=None)
-
-
-def format_datetime(datetime=None, format=None, rebase=True):
- """Return a date formatted according to the given pattern. If no
- :class:`~datetime.datetime` object is passed, the current time is
- assumed. By default rebasing happens which causes the object to
- be converted to the users's timezone (as returned by
- :func:`to_user_timezone`). This function formats both date and
- time.
-
- The format parameter can either be ``'short'``, ``'medium'``,
- ``'long'`` or ``'full'`` (in which cause the language's default for
- that setting is used, or the default from the :attr:`Babel.date_formats`
- mapping is used) or a format string as documented by Babel.
-
- This function is also available in the template context as filter
- named `datetimeformat`.
- """
- format = _get_format('datetime', format)
- return _date_format(dates.format_datetime, datetime, format, rebase)
-
-
-def format_date(date=None, format=None, rebase=True):
- """Return a date formatted according to the given pattern. If no
- :class:`~datetime.datetime` or :class:`~datetime.date` object is passed,
- the current time is assumed. By default rebasing happens which causes
- the object to be converted to the users's timezone (as returned by
- :func:`to_user_timezone`). This function only formats the date part
- of a :class:`~datetime.datetime` object.
-
- The format parameter can either be ``'short'``, ``'medium'``,
- ``'long'`` or ``'full'`` (in which cause the language's default for
- that setting is used, or the default from the :attr:`Babel.date_formats`
- mapping is used) or a format string as documented by Babel.
-
- This function is also available in the template context as filter
- named `dateformat`.
- """
- if rebase and isinstance(date, datetime):
- date = to_user_timezone(date)
- format = _get_format('date', format)
- return _date_format(dates.format_date, date, format, rebase)
-
-
-def format_time(time=None, format=None, rebase=True):
- """Return a time formatted according to the given pattern. If no
- :class:`~datetime.datetime` object is passed, the current time is
- assumed. By default rebasing happens which causes the object to
- be converted to the users's timezone (as returned by
- :func:`to_user_timezone`). This function formats both date and
- time.
-
- The format parameter can either be ``'short'``, ``'medium'``,
- ``'long'`` or ``'full'`` (in which cause the language's default for
- that setting is used, or the default from the :attr:`Babel.date_formats`
- mapping is used) or a format string as documented by Babel.
-
- This function is also available in the template context as filter
- named `timeformat`.
- """
- format = _get_format('time', format)
- return _date_format(dates.format_time, time, format, rebase)
-
-
-def format_timedelta(datetime_or_timedelta, granularity='second'):
- """Format the elapsed time from the given date to now or the given
- timedelta. This currently requires an unreleased development
- version of Babel.
-
- This function is also available in the template context as filter
- named `timedeltaformat`.
- """
- if isinstance(datetime_or_timedelta, datetime):
- datetime_or_timedelta = datetime.utcnow() - datetime_or_timedelta
- return dates.format_timedelta(datetime_or_timedelta, granularity,
- locale=get_locale())
-
-
-def _date_format(formatter, obj, format, rebase, **extra):
- """Internal helper that formats the date."""
- locale = get_locale()
- extra = {}
- if formatter is not dates.format_date and rebase:
- extra['tzinfo'] = get_timezone()
- return formatter(obj, format, locale=locale, **extra)
-
-
-def format_number(number):
- """Return the given number formatted for the locale in request
-
- :param number: the number to format
- :return: the formatted number
- :rtype: unicode
- """
- locale = get_locale()
- return numbers.format_number(number, locale=locale)
-
-
-def format_decimal(number, format=None):
- """Return the given decimal number formatted for the locale in request
-
- :param number: the number to format
- :param format: the format to use
- :return: the formatted number
- :rtype: unicode
- """
- locale = get_locale()
- return numbers.format_decimal(number, format=format, locale=locale)
-
-
-def format_currency(number, currency, format=None):
- """Return the given number formatted for the locale in request
-
- :param number: the number to format
- :param currency: the currency code
- :param format: the format to use
- :return: the formatted number
- :rtype: unicode
- """
- locale = get_locale()
- return numbers.format_currency(
- number, currency, format=format, locale=locale
- )
-
-
-def format_percent(number, format=None):
- """Return formatted percent value for the locale in request
-
- :param number: the number to format
- :param format: the format to use
- :return: the formatted percent number
- :rtype: unicode
- """
- locale = get_locale()
- return numbers.format_percent(number, format=format, locale=locale)
-
-
-def format_scientific(number, format=None):
- """Return value formatted in scientific notation for the locale in request
-
- :param number: the number to format
- :param format: the format to use
- :return: the formatted percent number
- :rtype: unicode
- """
- locale = get_locale()
- return numbers.format_scientific(number, format=format, locale=locale)
-
-
-def gettext(string, **variables):
- """Translates a string with the current locale and passes in the
- given keyword arguments as mapping to a string formatting string.
-
- ::
-
- gettext(u'Hello World!')
- gettext(u'Hello %(name)s!', name='World')
- """
- t = get_translations()
- if t is None:
- return string % variables
- return t.ugettext(string) % variables
-_ = gettext
-
-
-def ngettext(singular, plural, num, **variables):
- """Translates a string with the current locale and passes in the
- given keyword arguments as mapping to a string formatting string.
- The `num` parameter is used to dispatch between singular and various
- plural forms of the message. It is available in the format string
- as ``%(num)d`` or ``%(num)s``. The source language should be
- English or a similar language which only has one plural form.
-
- ::
-
- ngettext(u'%(num)d Apple', u'%(num)d Apples', num=len(apples))
- """
- variables.setdefault('num', num)
- t = get_translations()
- if t is None:
- return (singular if num == 1 else plural) % variables
- return t.ungettext(singular, plural, num) % variables
-
-
-def pgettext(context, string, **variables):
- """Like :func:`gettext` but with a context.
-
- .. versionadded:: 0.7
- """
- t = get_translations()
- if t is None:
- return string % variables
- return t.upgettext(context, string) % variables
-
-
-def npgettext(context, singular, plural, num, **variables):
- """Like :func:`ngettext` but with a context.
-
- .. versionadded:: 0.7
- """
- variables.setdefault('num', num)
- t = get_translations()
- if t is None:
- return (singular if num == 1 else plural) % variables
- return t.unpgettext(context, singular, plural, num) % variables
-
-
-def lazy_gettext(string, **variables):
- """Like :func:`gettext` but the string returned is lazy which means
- it will be translated when it is used as an actual string.
-
- Example::
-
- hello = lazy_gettext(u'Hello World')
-
- @app.route('/')
- def index():
- return unicode(hello)
- """
- from speaklater import make_lazy_string
- return make_lazy_string(gettext, string, **variables)
-
-
-def lazy_pgettext(context, string, **variables):
- """Like :func:`pgettext` but the string returned is lazy which means
- it will be translated when it is used as an actual string.
-
- .. versionadded:: 0.7
- """
- from speaklater import make_lazy_string
- return make_lazy_string(pgettext, context, string, **variables)
-
View
59 shake/cli/__init__.py
@@ -6,20 +6,35 @@
Command-line scripts
"""
-import os
+from os.path import sep, dirname, isfile, join, abspath, normpath, realpath
+import inflector
from pyceo import Manager, format_title
import voodoo
-from . import globals as g
from . import helpers as h
+ROOTDIR = normpath(abspath(realpath(join(dirname(__file__), '..', 'skeletons'))))
+APP_SKELETON = join(ROOTDIR, 'project')
+RESOURCE_SKELETON = join(ROOTDIR, 'resource')
+
+ENV_OPTIONS = {
+ 'autoescape': False,
+ 'block_start_string': '[%',
+ 'block_end_string': '%]',
+ 'variable_start_string': '[[',
+ 'variable_end_string': ']]',
+}
+
+FILTER = ('.pyc', '.DS_Store', '.pyo')
+
+
manager = Manager()
@manager.command
-def new(app_path='.', skeleton=g.APP_SKELETON, **options):
+def new(app_path='.', skeleton=APP_SKELETON, **options):
"""APP_PATH='.' [SKELETON_PATH]
The 'shake new' command creates a new Shake application with a default
@@ -34,13 +49,13 @@ def new(app_path='.', skeleton=g.APP_SKELETON, **options):
quiet = options.get('quiet', options.get('q', False))
pretend = options.get('pretend', options.get('p', False))
- app_path = app_path.rstrip(os.path.sep)
+ app_path = app_path.rstrip(sep)
data = {
'SECRET1': h.make_secret(),
'SECRET2': h.make_secret(),
}
voodoo.reanimate_skeleton(skeleton, app_path, data=data,
- filter_ext=g.FILTER, env_options=g.ENV_OPTIONS, **options)
+ filter_ext=FILTER, env_options=ENV_OPTIONS, **options)
if not pretend:
h.install_requirements(app_path, quiet)
@@ -53,7 +68,7 @@ def new(app_path='.', skeleton=g.APP_SKELETON, **options):
def add(name=None, *args, **options):
"""NAME [field:type, ...] [options]
- Generates the model, views and controller of a resource.
+ Generates the model, templates and view of a resource.
The resource is ready to use as a starting point for your RESTful,
resource-oriented application.
@@ -80,40 +95,40 @@ def add(name=None, *args, **options):
quiet = options.get('quiet', options.get('q', False))
pretend = options.get('pretend', options.get('p', False))
- name = name.rstrip(os.path.sep)
- singular, plural = h.sanitize_name(name)
+ name = name.rstrip(sep)
+ singular, plural, class_name = h.sanitize_name(name)
- bundle_src = os.path.join(g.RESOURCE_SKELETON, 'bundle')
- views_src = os.path.join(g.RESOURCE_SKELETON, 'views')
- bundle_dst = os.path.join('bundles', plural)
- views_dst = os.path.join('views', plural)
+ bundle_src = join(RESOURCE_SKELETON, 'bundle')
+ templates_src = join(RESOURCE_SKELETON, 'templates')
+ bundle_dst = join('bundles', plural)
+ templates_dst = join('templates', plural)
data = {
'singular': singular,
'plural': plural,
- 'class_name': h.underscores_to_camelcase(singular),
+ 'class_name': class_name,
'fields': h.get_model_fields(args),
}
# Bundle
if not quiet:
print voodoo.formatm('invoke', bundle_dst, color='white')
- bundle_dst = os.path.abspath(bundle_dst)
+ bundle_dst = abspath(bundle_dst)
voodoo.reanimate_skeleton(bundle_src, bundle_dst, data=data,
- filter_ext=g.FILTER, env_options=g.ENV_OPTIONS, **options)
+ filter_ext=FILTER, env_options=ENV_OPTIONS, **options)
- # Views
+ # templates
if not quiet:
- print voodoo.formatm('invoke', views_dst, color='white')
- views_dst = os.path.abspath(views_dst)
- voodoo.reanimate_skeleton(views_src, views_dst, data=data,
- filter_ext=g.FILTER, env_options=g.ENV_OPTIONS, **options)
+ print voodoo.formatm('invoke', templates_dst, color='white')
+ templates_dst = abspath(templates_dst)
+ voodoo.reanimate_skeleton(templates_src, templates_dst, data=data,
+ filter_ext=FILTER, env_options=ENV_OPTIONS, **options)
# Insert bundle import in urls.py
if not quiet:
print voodoo.formatm('update', 'urls.py', color='green')
- path = os.path.abspath('urls.py')
- if not os.path.isfile(path):
+ path = abspath('urls.py')
+ if not isfile(path):
if not quiet:
print voodoo.formatm('warning', 'urls.py not found', color='yellow')
return
View
65 shake/cli/globals.py
@@ -1,65 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
- Shake.cli.globals
- --------------------------
-
-"""
-import os
-
-
-ROOTDIR = os.path.realpath(os.path.join(os.path.dirname(__file__),
- '..', 'skeletons'))
-
-APP_SKELETON = os.path.join(ROOTDIR, 'project')
-RESOURCE_SKELETON = os.path.join(ROOTDIR, 'resource')
-
-
-ENV_OPTIONS = {
- 'autoescape': False,
- 'block_start_string': '[%',
- 'block_end_string': '%]',
- 'variable_start_string': '[[',
- 'variable_end_string': ']]',
-}
-
-FILTER = ('.pyc', '.DS_Store', '.pyo')
-
-
-PLURAL_RULES = [
- ('[ml]ouse$', 'ouse$', 'ice'),
- ('child$', '$', 'ren'),
- ('foot$', 'foot$', 'feet'),
- ('booth$', '$', 's'),
- ('ooth$', 'ooth$', 'eeth'),
- ('octopus$', 'us$', 'i'),
- ('l[eo]af$', 'f$', 'ves'),
- ('ife$', 'fe$', 'ves'),
- ('sis$', 'is$', 'es'),
- ('man$', 'an$', 'en'),
- ('eau$', '$', 'x'),
- ('lf$', 'f$', 'ves'),
- ('[sxz]$', '$', 'es'),
- ('[^aeioudgkprt]h$', '$', 'es'),
- ('(qu|[^aeiou])y$', 'y$', 'ies'),
- ('[^s]$', '$', 's'),
-]
-
-SINGULAR_RULES = [
- ('[ml]ice$', 'ice$', 'ouse'),
- ('children$', 'ren$', ''),
- ('feet$', 'eet$', 'oot'),
- ('booths$', 's$', ''),
- ('eeth$', 'eeth$', 'ooth'),
- ('octopi$', 'i$', 'us'),
- ('l[eo]aves$', 'ves$', 'f'),
- ('ives$', 'ves$', 'fe'),
- ('ses$', 'es$', 'is'),
- ('men$', 'en$', 'an'),
- ('eaux$', 'x$', ''),
- ('lves$', 'ves$', 'f'),
- ('[sxz]es$', 'es$', ''),
- ('[^aeioudgkprt]hes$', 'es$', ''),
- ('(qu|[^aeiou])ies$', 'ies$', 'y'),
- ('s$', 's$', ''),
-]
-
View
47 shake/cli/helpers.py
@@ -10,13 +10,12 @@
from subprocess import Popen
import re
+import inflector
import voodoo
-from .globals import SINGULAR_RULES, PLURAL_RULES
+inf = inflector.English()
-_FIRST_CAP_RE = re.compile('(.)([A-Z][a-z]+)')
-_ALL_CAP_RE = re.compile('([a-z0-9])([A-Z])')
_IMPORTS_RE = re.compile(r'"(\n*\s*(#[^\n]*|(from [a-zA-Z0-9_\.]+\s+)?import\s+.*))+')
@@ -35,51 +34,19 @@ def install_requirements(app_path, quiet=False):
proc.communicate()
-def underscores_to_camelcase(name):
- return ''.join([word.title() for word in name.split('_')])
-
-
-def camelcase_to_underscores(name):
- s1 = _FIRST_CAP_RE.sub(r'\1_\2', name)
- name = _ALL_CAP_RE.sub(r'\1_\2', s1).lower()
- return name
-
-
-def regex_rules(rules):
- for line in rules:
- pattern, search, replace = line
- yield lambda word: re.search(pattern, word) and \
- re.sub(search, replace, word)
-
-
-def singularize(noun):
- for rule in regex_rules(SINGULAR_RULES):
- result = rule(noun)
- if result:
- return result
- return noun
-
-
-def pluralize(noun):
- for rule in regex_rules(PLURAL_RULES):
- result = rule(noun)
- if result:
- return result
- return noun
-
-
def sanitize_name(name):
- singular = singularize(name)
+ singular = inf.singularize(name)
plural = name
if singular == name:
- plural = pluralize(name)
+ plural = inf.pluralize(name)
num = 2
while os.path.exists(plural + '.py'):
plural = plural + str(num)
num = num + 1
-
- return singular, plural
+
+ class_name = inf.tableize(singular)
+ return singular, plural, class_name
def get_model_fields(args):
View
14 shake/config.py
@@ -13,7 +13,7 @@ class DefaultSettings(object):
SERVER_NAME = '0.0.0.0'
SERVER_PORT = 5000
- DEFAULT_SUBDOMAIN = ''
+ DEFAULT_SUBDOMAIN = None
FORCE_SCRIPT_NAME = False
@@ -29,16 +29,14 @@ class DefaultSettings(object):
# The maximum size for regular form data (not files)
MAX_FORM_MEMORY_SIZE = 1024 * 1024 * 2 # 2 MB
+ DEFAULT_MIMETYPE = 'text/html'
+
DEFAULT_LOCALE = 'en'
DEFAULT_TIMEZONE = 'UTC'
-
- # URL prefix for static files.
- # Examples: "http://media.lucumalabs.com/static/", "http://abc.org/static/"
- STATIC_URL = '/static'
- PAGE_NOT_FOUND = 'shake.controllers.not_found_page'
- PAGE_ERROR = 'shake.controllers.error_page'
- PAGE_NOT_ALLOWED = 'shake.controllers.not_allowed_page'
+ PAGE_NOT_FOUND = 'shake.views.not_found_page'
+ PAGE_ERROR = 'shake.views.error_page'
+ PAGE_NOT_ALLOWED = 'shake.views.not_allowed_page'
QUOTES = [
# quote, by
View
89 shake/controllers.py
@@ -1,89 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
- Shake.controllers
- --------------------------
-
- Generic controllers
-
-"""
-from random import choice
-
-from .helpers import local, NotFound, safe_join, send_file
-from .views import default_render
-
-
-__all__ = (
- 'not_found_page', 'error_page', 'not_allowed_page', 'render_view',
-)
-
-
-def not_found_page(request, error):
- """Default "Not Found" page.
-
- """
- rules = local.urls.map._rules
- return default_render('error_notfound.html', locals())
-
-
-def error_page(request, error):
- """A generic error page.
-
- """
- return default_render('error.html')
-
-
-def not_allowed_page(request, error):
- """A default "access denied" page.
-
- """
- return default_render('error_notallowed.html')
-
-
-def render_view(request, render, view, context=None, **kwargs):
- """A really simple controller who render directly a view.
-
- render
- : the renderer to use.
- view
- : the view to render.
- context
- : values to add to the view context.
-
- """
- return render(view, context, **kwargs)
-
-
-def send_from_directory(request, directory, filename, **options):
- """Send a file from a given directory with `send_file`. This
- is a secure way to quickly expose static files from an upload folder
- or something similar.
-
- Example usage:
-
- @app.route('/uploads/<path:filename>')
- def download_file(filename):
- return send_from_directory(UPLOAD_FOLDER, filename,
- as_attachment=True)
-
- 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.
-
- directory
- : the directory where all the files are stored.
- filename
- : the filepath relative to that directory to download.
- options
- : optional keyword arguments that are directly forwarded to `send_file`.
-
- --------------------------------
- Copied almost unchanged from Flask <http://flask.pocoo.org/>
- Copyright © 2010 by Armin Ronacher.
- Used under the modified BSD license.
- """
- filepath = safe_join(directory, filename)
- if not os.path.isfile(filepath):
- raise NotFound
- return send_file(request, filepath, conditional=True, **options)
-
View
0  shake/default_views/error.html → shake/default_templates/error.html
File renamed without changes
View
0  shake/default_views/error_notallowed.html → shake/default_templates/error_notallowed.html
File renamed without changes
View
0  shake/default_views/error_notfound.html → shake/default_templates/error_notfound.html
File renamed without changes
View
0  shake/default_views/shake_default_view.css → shake/default_templates/shake_default_view.css
File renamed without changes
View
2  shake/helpers.py
@@ -52,7 +52,7 @@ def url_for(endpoint, anchor=None, method=None, external=False, **values):
urls = local.urls
except AttributeError:
raise RuntimeError("You must call this function only from"
- " inside a controller or a view")
+ " inside a view or a template")
try:
url = urls.build(endpoint, values, method=method,
force_external=external)
View
513 shake/i18n.py
@@ -0,0 +1,513 @@
+# -*- coding: utf-8 -*-
+"""
+ Shake.i18n
+ --------------------------
+
+ Implements i18n/l10n support for Shake applications based on Babel and pytz.
+
+ ----------------
+ Some portions derived from Flask-Babel (c) 2010 by Armin Ronacher.
+ Used under the modified BSD license.
+
+"""
+from __future__ import absolute_import
+import os
+
+# Workaround for a OSX bug
+if os.environ.get('LC_CTYPE', '').lower() == 'utf-8':
+ os.environ['LC_CTYPE'] = 'en_US.utf-8'
+
+from collections import defaultdict
+from datetime import datetime
+import io
+from os.path import join, dirname, realpath, abspath, normpath, isdir, isfile
+
+from babel import dates, numbers, support, Locale
+from jinja2 import Markup
+from pytz import timezone, UTC
+from werkzeug import ImmutableDict
+import yaml
+
+
+LOCALES_DIR = 'locales'
+
+
+class I18n(object):
+ """Internationalization system
+ """
+
+ default_date_formats = ImmutableDict({
+ 'time': 'medium',
+ 'date': 'medium',
+ 'datetime': 'medium',
+ 'time.short': None,
+ 'time.medium': None,
+ 'time.full': None,
+ 'time.long': None,
+ 'date.short': None,
+ 'date.medium': None,
+ 'date.full': None,
+ 'date.long': None,
+ 'datetime.short': None,
+ 'datetime.medium': None,
+ 'datetime.full': None,
+ 'datetime.long': None,
+ })
+
+
+ def __init__(self, locales_dirs=None, app=None, date_formats=None):
+ """
+
+ locales_dirs
+ : list of paths that will be searched, in order, for the locales
+ app
+ : a `Shake` instance.
+ date_formats
+ : defaults date formats.
+
+ """
+ self.translations = {}
+
+ locales_dirs = locales_dirs or [LOCALES_DIR]
+ search_paths = []
+ for p in locales_dirs:
+ p = normpath(abspath(realpath(p)))
+ if not isdir(p):
+ p = dirname(p)
+ search_paths.append(p)
+ self.search_paths = search_paths
+ self.date_formats = self.default_date_formats.copy()
+ if app:
+ self.init_app(app)
+
+
+ def init_app(self, app):
+ """
+ """
+ self.app = app
+
+
+ @property
+ def default_locale(self):
+ """The default locale from the configuration as a string.
+
+ """
+ return self.app.settings.DEFAULT_LOCALE
+
+
+ @property
+ def default_timezone(self):
+ """The default timezone from the configuration as instance of a
+ `pytz.timezone` object.
+
+ """
+ return timezone(self.app.settings.DEFAULT_TIMEZONE)
+
+
+ def get_str_locale(self):
+ """Returns the locale that should be used for this request as
+ a string. This returns the default locale if used outside of a request.
+
+ """
+ locale = local.request and local.request.get_locale()
+ locale = locale or self.default_locale
+ return locale.split('.')[0].replace('_', '-')
+
+
+ def get_locale(self):
+ """Returns the locale that should be used for this request as
+ an instance of `Babel.Locale`.
+ This returns the default locale if used outside of a request.
+
+ """
+ slocale = self.get_str_locale()
+ return Locale.parse(slocale, sep='-')
+
+
+ def get_timezone(self):
+ """Returns the timezone that should be used for this request as
+ `pytz.timezone` object. This returns the default timezone if used
+ outside of a request or if no timezone was defined.
+
+ """
+ tzinfo = local.request and local.request.tzinfo
+ if not tzinfo:
+ tzinfo = self.default_timezone
+ elif isinstance(tzinfo, basestring):
+ tzinfo = timezone(tzinfo)
+ return tzinfo
+
+
+ def translate(self, key, count=None, locale=None, **kwargs):
+ """Load the translation for the given key using the current locale.
+
+ If the value is a dictionary, and `count` is defined, uses the value
+ whose key is that number. If that key doesn't exist, a `'n'` key
+ is tried instead. If that doesn't exits either, an empty string is
+ returned.
+
+ The final value is formatted using `kwargs` (and also `count` if
+ available) so the format placeholders must be named instead of
+ positional.
+
+ If the value isn't a dictionary or a string, is returned as is.
+
+ Examples:
+
+ >>> translate('hello_world')
+ 'hello %(what)s'
+ >>> translate('hello_world', what='world')
+ 'hello world'
+ >>> translate('a_list', what='world')
+ ['a', 'b', 'c']
+
+ """
+ key = str(key)
+ locale = locale or self.get_str_locale()
+ value = self.key_lookup(key)
+ if not value:
+ return Markup('<missing:%s>' % (key, ))
+
+ if isinstance(value, dict):
+ value = self.pluralize(value, count)
+
+ if isinstance(value, basestring):
+ value = value % kwargs
+ if key.endswith('_html'):
+ return Markup(value)
+
+ return value
+
+
+ def key_lookup(self, key):
+ """
+ """
+ path, subkey = self._find_keypath(key)
+ if not (path and subkey):
+ return None
+
+ value = self._load_language(path, locale)
+
+ try:
+ for k in subkey.split('.'):
+ value = value.get(k)
+ return value
+ except (IndexError, ValueError), e:
+ return None
+
+
+ def _find_keypath(self, key):
+ if ':' not in key:
+ return self.search_paths[0], key
+
+ path, subkey = key.split(':', 1)
+ lpath = path.split('.')
+
+ for root in self.search_paths:
+ dirname, dirnames, filenames = os.walk(root).next()
+ if lpath[0] in dirnames:
+ break
+ else:
+ return None, None
+
+ path = join(root, *lpath)
+ if not isdir(path):
+ return None, None
+ return path, subkey
+
+
+ def _load_language(self, path, locale):
+ """From the given `path`, load the language file for the current or
+ given locale. If the locale has a regional part (eg: 'en-US') the
+ language-wide version will be tried as well (eg: 'en') if the first
+ is not found.
+
+ """
+ filenames = [locale]
+ if '-' in locale:
+ filenames.append(locale.split()[0])
+ filenames.append(self.default_locale)
+
+ for filename in filenames:
+ cache_key = join(path, filename)
+ cached = self.translations.get(cache_key)
+ if cached:
+ return cached
+ filename = cache_key + '.yml'
+ if isfile(filename):
+ break
+ else:
+ return
+ try:
+ with io.open(filename) as f:
+ data = yaml.load(f)
+ self.translations[cache_key] = data
+ return data
+ except (IOError, AttributeError):
+ return
+
+
+ def pluralize(self, d, count):
+ """Takes a dictionary and a number and return the value whose key in
+ the dictionary is that number. If that key doesn't exist, a `'n'` key
+ is tried instead. If that doesn't exits either, an empty string is
+ returned. Examples:
+
+ >>> i18n = I18n()
+ >>> d = {
+ 0: 'No apples'
+ 1: 'One apple',
+ 3: 'Few apples',
+ 'n': '%(count)s apples',
+ }
+ >>> i18n.pluralize(d, 0)
+ 'No apples'
+ >>> i18n.pluralize(d, 1)
+ 'One apple'
+ >>> i18n.pluralize(d, 3)
+ 'Few apples'
+ >>> i18n.pluralize(d, 10)
+ '10 apples'
+ >>> i18n.pluralize({0: 'off', 'n': 'on'}, 3)
+ 'on'
+ >>> i18n.pluralize({0: 'off', 'n': 'on'}, 0)
+ 'off'
+ >>> i18n.pluralize({}, 3)
+ ''
+
+ """
+ en_count = str(count)
+ if count is None:
+ count = 0
+ if isinstance(count, int):
+ en_count = number_to_english(count)
+ return d.get(count, d.get(en_count, d.get('other', u'')))
+
+
+ def _get_format(self, key, format):
+ """A small helper for the datetime formatting functions. Looks up
+ format defaults for different kinds.
+
+ """
+ if format is None:
+ format = self.date_formats.get(key)
+ if format in ('short', 'medium', 'full', 'long'):
+ rv = self.date_formats['%s.%s' % (key, format)]
+ if rv is not None:
+ format = rv
+ return format
+
+
+ def to_user_timezone(self, datetime):
+ """Convert a datetime object to the user's timezone. This automatically
+ happens on all date formatting unless rebasing is disabled. If you need
+ to convert a :class:`datetime.datetime` object at any time to the user's
+ timezone (as returned by `get_timezone` this function can be used).
+
+ """
+ if datetime.tzinfo is None:
+ datetime = datetime.replace(tzinfo=UTC)
+ tzinfo = self.get_timezone()
+ return tzinfo.normalize(datetime.astimezone(tzinfo))
+
+
+ def to_utc(self, datetime):
+ """Convert a datetime object to UTC and drop tzinfo. This is the
+ opposite operation to `to_user_timezone`.
+
+ """
+ if datetime.tzinfo is None:
+ datetime = self.get_timezone().localize(datetime)
+ return datetime.astimezone(UTC).replace(tzinfo=None)
+
+
+ def format_datetime(self, datetime=None, format=None, rebase=True):
+ """Return a date formatted according to the given pattern. If no
+ `datetime.datetime` object is passed, the current time is
+ assumed. By default rebasing happens which causes the object to
+ be converted to the users's timezone (as returned by
+ `to_user_timezone`). This function formats both date and
+ time.
+
+ The format parameter can either be `'short'`, `'medium'`,
+ `'long'` or `'full'` (in which cause the language's default for
+ that setting is used, or the default from the `Babel.date_formats`
+ mapping is used) or a format string as documented by Babel.
+
+ This function is also available in the template context as filter
+ named `datetimeformat`.
+
+ """
+ format = self._get_format('datetime', format)
+ return self._date_format(dates.format_datetime, datetime, format, rebase)
+
+
+ def format_date(self, date=None, format=None, rebase=True):
+ """Return a date formatted according to the given pattern. If no
+ `datetime.datetime` or `datetime.date` object is passed,
+ the current time is assumed. By default rebasing happens which causes
+ the object to be converted to the users's timezone (as returned by
+ `to_user_timezone`). This function only formats the date part
+ of a `datetime.datetime` object.
+
+ The format parameter can either be `'short'`, `'medium'`,
+ `'long'` or `'full'` (in which cause the language's default for
+ that setting is used, or the default from the `Babel.date_formats`
+ mapping is used) or a format string as documented by Babel.
+
+ This function is also available in the template context as filter
+ named `dateformat`.
+
+ """
+ if rebase and isinstance(date, datetime):
+ date = self.to_user_timezone(date)
+ format = self._get_format('date', format)
+ return self._date_format(dates.format_date, date, format, rebase)
+
+
+ def format_time(self, time=None, format=None, rebase=True):
+ """Return a time formatted according to the given pattern. If no
+ `datetime.datetime` object is passed, the current time is
+ assumed. By default rebasing happens which causes the object to
+ be converted to the users's timezone (as returned by
+ `to_user_timezone`). This function formats both date and
+ time.
+
+ The format parameter can either be `'short'`, `'medium'`,
+ `'long'` or `'full'` (in which cause the language's default for
+ that setting is used, or the default from the Babel.date_formats`
+ mapping is used) or a format string as documented by Babel.
+
+ This function is also available in the template context as filter
+ named `timeformat`.
+
+ """
+ format = _get_format('time', format)
+ return self._date_format(dates.format_time, time, format, rebase)
+
+
+ def format_timedelta(self, datetime_or_timedelta, granularity='second'):
+ """Format the elapsed time from the given date to now or the given
+ timedelta.
+
+ This function is also available in the template context as filter
+ named `timedeltaformat`.
+
+ """
+ locale = self.get_locale()
+ if isinstance(datetime_or_timedelta, datetime):
+ datetime_or_timedelta = datetime.utcnow() - datetime_or_timedelta
+ return dates.format_timedelta(datetime_or_timedelta, granularity,
+ locale=locale)
+
+
+ def _date_format(formatter, obj, format, rebase, **extra):
+ """Internal helper that formats the date."""
+ locale = self.get_locale()
+ extra = {}
+ if formatter is not dates.format_date and rebase:
+ extra['tzinfo'] = self.get_timezone()
+ return formatter(obj, format, locale=locale, **extra)
+
+
+ def format_number(self, number):
+ """Return the given number formatted for the locale in the
+ current request.
+
+ number
+ : the number to format
+
+ return (unicode)
+ : the formatted number
+
+ This function is also available in the template context as filter
+ named `numberformat`.
+
+ """
+ locale = self.get_locale()
+ return numbers.format_number(number, locale=locale)
+
+
+ def format_decimal(self, number, format=None):
+ """Return the given decimal number formatted for the locale in the
+ current request.
+
+ number
+ : the number to format
+ format
+ : the format to use
+
+ return (unicode)
+ : the formatted number
+
+ This function is also available in the template context as filter
+ named `decimalformat`.
+
+ """
+ locale = self.get_locale()
+ return numbers.format_decimal(number, format=format, locale=locale)
+
+
+ def format_currency(self, number, currency, format=None):
+ """Return the given number formatted for the locale in the
+ current request.
+
+ number
+ : the number to format
+ currency
+ : the currency code
+ format
+ : the format to use
+
+ return (unicode)
+ : the formatted number
+
+ This function is also available in the template context as filter
+ named `currencyformat`.
+
+ """
+ locale = self.get_locale()
+ return numbers.format_currency(
+ number, currency, format=format, locale=locale
+ )
+
+
+ def format_percent(self, number, format=None):
+ """Return a percent value formatted for the locale in the
+ current request.
+
+ number
+ : the number to format
+ format
+ : the format to use
+
+ return (unicode)
+ : the formatted percent number
+
+ This function is also available in the template context as filter
+ named `percentformat`.
+
+ """
+ locale = self.get_locale()
+ return numbers.format_percent(number, format=format, locale=locale)
+
+
+ def format_scientific(self, number, format=None):
+ """Return value formatted in scientific notation for the locale in
+ the current request.
+
+ number
+ : the number to format
+ format
+ : the format to use
+
+ return (unicode)
+ : the formatted percent number
+
+ This function is also available in the template context as filter
+ named `scientificformat`.
+
+ """
+ locale = self.get_locale()
+ return numbers.format_scientific(number, format=format, locale=locale)
+
View
171 shake/render.py
@@ -0,0 +1,171 @@
+# -*- coding: utf-8 -*-
+"""
+ Shake.render
+ --------------------------
+
+ Implements the bridge to Jinja2.
+
+"""
+from collections import defaultdict
+from datetime import datetime
+import io
+from os.path import isdir, dirname, join, abspath, normpath, realpath
+
+import jinja2
+from werkzeug.local import LocalProxy
+
+from .helpers import url_for, local
+from .session import get_csrf, get_messages
+from .templates import link_to, dumb_plural
+from .wrappers import Response, make_response
+
+
+__all__ = (
+ 'Render',
+)
+
+
+TEMPLATES_DIR = 'templates'
+
+
+class Render(object):
+ """A thin wrapper arround Jinja2.
+ """
+
+ default_globals = {
+ 'now': LocalProxy(datetime.utcnow),
+ 'ellipsis': Ellipsis,
+ 'enumerate': enumerate,
+
+ 'request': local('request'),
+ 'settings': local('app.settings'),
+ 'csrf': LocalProxy(get_csrf),
+ 'url_for': url_for,
+ 'get_messages': get_messages,
+
+ 'link_to': link_to,
+ 'plural': dumb_plural,
+
+ # Deprecated. Will go away in v1.3
+ 'csrf_secret': LocalProxy(get_csrf),
+ }
+
+ default_extensions = []
+
+ default_filters = {}
+
+ default_tests = {
+ 'ellipsis': (lambda obj: obj == Ellipsis),
+ }
+
+
+ def __init__(self, templates_path=None, loader=None, i18n=None,
+ default_mimetype='text/html', response_class=Response, **kwargs):
+ """
+
+ templates_path
+ : optional path to the folder from where the templates will be loaded.
+ Internally a `jinja2.FileSystemLoader` will be constructed.
+ You can ignore this parameter and provide a `loader` instead.
+ loader
+ : optional replacement loader for the templates. If provided,
+ `templates_path` is ignored.
+ i18n
+ : optional `I18n` instance for adding internationalization filters.
+ default_mimetype
+ : the default MIMETYPE of the response.
+ response_class
+ : the `Response` class used by `render`.
+ kwargs
+ : extra parameters passed directly to the `jinja2.Environment`
+ constructor.
+
+ """
+ if not loader:
+ templates_path = templates_path or TEMPLATES_DIR
+ templates_path = normpath(abspath(realpath(templates_path)))
+ # Instead of a path, we've probably recieved the value of __file__
+ if not isdir(templates_path):
+ templates_path = join(dirname(templates_path), TEMPLATES_DIR)
+ loader = jinja2.FileSystemLoader(templates_path)
+
+ tglobals = kwargs.pop('globals', {})
+ tfilters = kwargs.pop('filters', {})
+ ttests = kwargs.pop('tests', {})
+ kwargs.setdefault('extensions', self.default_extensions)
+ kwargs.setdefault('autoescape', True)
+
+ env = jinja2.Environment(loader=loader, **kwargs)
+
+ env.globals.update(self.default_globals)
+ env.globals.update(tglobals)
+ env.globals.update(self.default_filters)
+ env.filters.update(tfilters)
+ env.tests.update(self.default_tests)
+ env.tests.update(ttests)
+
+ self.env = env
+ self.default_mimetype = default_mimetype
+ self.response_class = response_class
+
+ if i18n:
+ self.init_i18n(i18n)
+
+
+ def init_i18n(self, i18n):
+ self.env.globals['t'] = i18n.translate
+ self.env.filters.update({
+ 'datetimeformat': i18n.format_datetime,
+ 'dateformat': i18n.format_date,
+ 'timeformat': i18n.format_time,
+ 'timedeltaformat': i18n.format_timedelta,
+ 'numberformat': i18n.format_number,
+ 'decimalformat': i18n.format_decimal,
+ 'currencyformat': i18n.format_currency,
+ 'percentformat': i18n.format_percent,
+ 'scientificformat': i18n.format_scientific,
+ })
+
+
+ def render(self, tmpl, context=None, to_string=False, **kwargs):
+ """Render a template `tmpl` using the given `context`.
+ If `to_string` is True, the result is returned as is.
+ If not, is used to build a response along with the other parameters.
+
+ """
+ context = context or {}
+ result = tmpl.render(context)
+ if to_string:
+ return result
+ kwargs.setdefault('mimetype', self.default_mimetype)
+ return make_response(result, response_class=self.response_class, **kwargs)
+
+
+ def __call__(self, filename, context=None, to_string=False, **kwargs):
+ """Load a template from `<templates_path>/<filename>` and passes it to
+ `render` along with the other parameters.
+
+ Depending of the value of `to_string`, returns the rendered template as
+ a string or as a response_class instance.
+
+ """
+ tmpl = self.env.get_template(filename)
+ return self.render(tmpl, context=context, to_string=to_string, **kwargs)