├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── MANIFEST.in ├── Makefile ├── README.md ├── flask_humanize ├── __init__.py └── compat.py ├── requirements ├── main.txt └── test.txt ├── setup.py └── tests ├── conftest.py └── test_humanize.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Python bytecode / optimized files 2 | *.py[co] 3 | *.egg-info 4 | build 5 | dist 6 | venv 7 | 8 | # Sphinx documentation 9 | docs/_build 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 2.6 5 | - 2.7 6 | - 3.3 7 | - 3.4 8 | - pypy 9 | 10 | install: 11 | - pip install -q -r requirements/main.txt 12 | - pip install -q -r requirements/test.txt 13 | - pip install -q -e . 14 | 15 | script: 16 | - py.test tests 17 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.3.0 (2016-03-26) 5 | ------------------ 6 | 7 | - ``PyPI`` package has been renamed into ``Flask-Humanize`` (`#4`_). Thanks to 8 | `@singingwolfboy`_, `@eg0r`_ and `@egorsmkv`_. 9 | 10 | .. _#4: https://github.com/vitalk/flask-humanize/issues/4 11 | .. _@singingwolfboy: https://github.com/singingwolfboy 12 | .. _@egorsmkv: https://github.com/egorsmkv 13 | .. _@eg0r: https://github.com/eg0r 14 | 15 | 0.2.0 (2016-01-25) 16 | ------------------ 17 | 18 | - Support UTC time. Thanks to `@diginikkari`_ for the complete PR (`#2`_). 19 | 20 | .. _#2: https://github.com/vitalk/flask-humanize/pull/2 21 | .. _@diginikkari: https://github.com/diginikkari 22 | 23 | 0.1.0 (2015-11-04) 24 | ------------------ 25 | 26 | - Set package status to beta. 27 | 28 | - Unset locale after each request. 29 | 30 | - Fix compatibility with python 2.6 version. 31 | 32 | - Add test suite; use ``pytest-flask`` for testing. 33 | 34 | 0.0.1 (2015-10-29) 35 | ------------------ 36 | 37 | First release on PyPi. 38 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include Makefile 3 | include *.rst 4 | recursive-include tests *.py 5 | recursive-include requirements *.txt 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | @rm -rf build dist *.egg-info 3 | @find . -name '*.py?' -delete 4 | 5 | 6 | .PHONY: clean 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask Humanize 2 | 3 | Provides an interface between [Flask](http://flask.pocoo.org/) web framework 4 | and [humanize](https://github.com/jmoiron/humanize) library. 5 | 6 | ## Features 7 | 8 | - Add new filter `humanize` to jinja environment, which can be easily used for 9 | humanize different objects: 10 | 11 | + Integer numbers: 12 | 13 | ```jinja 14 | {{ 12345|humanize('intcomma') }} -> 12,345 15 | {{ 12345591313|humanize('intword') }} -> 12.3 billion 16 | {{ 5|humanize('apnumber') }} -> five 17 | ``` 18 | 19 | + Floating point numbers: 20 | 21 | ```jinja 22 | {{ 0.3|humanize('fractional') }} -> 1/3 23 | {{ 1.5|humanize('fractional') }} -> 1 1/2 24 | ``` 25 | 26 | + File sizes: 27 | 28 | ```jinja 29 | {{ 1000000|humanize('naturalsize') }} -> 1.0 MB 30 | {{ 1000000|humanize('naturalsize', binary=True) }} -> 976.6 KiB 31 | ``` 32 | 33 | + Date & times: 34 | 35 | ```jinja 36 | {{ datetime.datetime.now()|humanize('naturalday') }} -> today 37 | {{ datetime.date(2014,4,21)|humanize('naturaldate') }} -> Apr 21 2014 38 | {{ (datetime.datetime.now() - datetime.timedelta(hours=1))|humanize() }} -> an hour ago 39 | ``` 40 | 41 | - Runtime i18n/l10n 42 | 43 | ```python 44 | from flask import Flask 45 | from flask_humanize import Humanize 46 | 47 | app = Flask(__name__) 48 | humanize = Humanize(app) 49 | 50 | @humanize.localeselector 51 | def get_locale(): 52 | return 'ru_RU' 53 | ``` 54 | 55 | ```jinja 56 | {{ datetime.datetime.now()|humanize }} -> сейчас 57 | ``` 58 | 59 | - In order to use UTC time instead of local time for humanize date and time 60 | methods use `HUMANIZE_USE_UTC` option, which is disabled by default: 61 | 62 | ```python 63 | HUMANIZE_USE_UTC = True 64 | ``` 65 | 66 | ## Issues 67 | 68 | Don't hesitate to open [GitHub Issues](https://github.com/vitalk/flask-humanize/issues) for any bug or suggestions. 69 | -------------------------------------------------------------------------------- /flask_humanize/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | flask_humanize.py 5 | ~~~~~~~~~~~~~~~~~ 6 | 7 | Add common humanization utilities, like turning number into a fuzzy 8 | human-readable duration or into human-readable size, to your flask 9 | applications. 10 | 11 | :copyright: (c) by Vital Kudzelka 12 | :license: MIT 13 | """ 14 | from datetime import datetime 15 | import humanize 16 | from flask import current_app 17 | from werkzeug.datastructures import ImmutableDict 18 | 19 | from . import compat 20 | 21 | 22 | __version__ = '0.3.0' 23 | 24 | 25 | def force_unicode(value): 26 | if not isinstance(value, compat.text_type): 27 | return value.decode('utf-8') 28 | return value 29 | 30 | 31 | def app_has_babel(app): 32 | """Check application instance for configured babel extension.""" 33 | obj = app.extensions.get('babel') 34 | return obj is not None 35 | 36 | 37 | def self_name(string): 38 | """Create config key for extension.""" 39 | return 'HUMANIZE_{0}'.format(string.upper()) 40 | 41 | 42 | default_config = ImmutableDict({ 43 | # The default locale to work with. When `BABEL_DEFAULT_LOCALE` is 44 | # available then it used instead. 45 | 'default_locale': 'en', 46 | 47 | # Use UTC instead of local time for humanize dates and times. 48 | 'use_utc': False, 49 | }) 50 | 51 | 52 | class Humanize(object): 53 | """Add common humanization utilities, like turning number into a fuzzy 54 | human-readable duration or into human-readable size, to your flask 55 | applications. 56 | """ 57 | 58 | # A function uses for locale selection. 59 | locale_selector_func = None 60 | 61 | def __init__(self, app=None): 62 | self.app = app 63 | if app is not None: 64 | self.init_app(app) 65 | 66 | def init_app(self, app): 67 | """Initialize application to use with extension. 68 | 69 | :param app: The Flask instance. 70 | 71 | Example:: 72 | 73 | from myapp import create_app() 74 | from flask_humanize import Humanize 75 | 76 | app = create_app() 77 | humanize.init_app(app) 78 | 79 | """ 80 | for k, v in default_config.items(): 81 | app.config.setdefault(self_name(k), v) 82 | 83 | if app_has_babel(app): 84 | default_locale = app.config['BABEL_DEFAULT_LOCALE'] 85 | if default_locale is not None: 86 | app.config.setdefault(self_name('default_locale'), default_locale) 87 | 88 | if app.config.get('HUMANIZE_USE_UTC'): 89 | # Override humanize.time._now to use UTC time 90 | humanize.time._now = datetime.utcnow 91 | else: 92 | humanize.time._now = datetime.now 93 | 94 | app.add_template_filter(self._humanize, 'humanize') 95 | app.before_request(self._set_locale) 96 | app.after_request(self._unset_locale) 97 | 98 | if not hasattr(app, 'extensions'): 99 | app.extensions = {} 100 | app.extensions['humanize'] = self 101 | 102 | @property 103 | def default_locale(self): 104 | """Returns the default locale for current application.""" 105 | return current_app.config['HUMANIZE_DEFAULT_LOCALE'] 106 | 107 | def localeselector(self, func): 108 | """Registers a callback function for locale selection. 109 | 110 | Callback is a callable which returns the locale as string, 111 | e.g. ``'en_US'``, ``'ru_RU'``:: 112 | 113 | from flask import request 114 | 115 | @humanize.localeselector 116 | def get_locale(): 117 | return request.accept_languages.best_match(available_locales) 118 | 119 | When no callback is available or `None` is returned, then locale 120 | falls back to the default one from application configuration. 121 | """ 122 | self.locale_selector_func = func 123 | return func 124 | 125 | def _set_locale(self): 126 | if self.locale_selector_func is None: 127 | locale = self.default_locale 128 | else: 129 | locale = self.locale_selector_func() 130 | if locale is None: 131 | locale = self.default_locale 132 | 133 | try: 134 | humanize.i18n.activate(locale) 135 | except IOError: 136 | pass 137 | 138 | def _unset_locale(self, response): 139 | humanize.i18n.deactivate() 140 | return response 141 | 142 | def _humanize(self, value, fname='naturaltime', **kwargs): 143 | try: 144 | method = getattr(humanize, fname) 145 | except AttributeError: 146 | raise Exception( 147 | "Humanize module does not contains function '%s'" % fname 148 | ) 149 | 150 | try: 151 | value = method(value, **kwargs) if kwargs else method(value) 152 | except Exception: 153 | raise ValueError( 154 | "An error occured during execution function '%s'" % fname 155 | ) 156 | 157 | return force_unicode(value) 158 | -------------------------------------------------------------------------------- /flask_humanize/compat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | 5 | 6 | PY2 = sys.version_info[0] == 2 7 | 8 | if PY2: 9 | text_type = unicode 10 | else: 11 | text_type = str 12 | -------------------------------------------------------------------------------- /requirements/main.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | humanize>=0.5.1 3 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | pytest>=2.8.2 2 | pytest-flask>=0.10.0 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Flask-Humanize 5 | ============== 6 | 7 | Extension provides common humanization utilities, like turning number into a 8 | fuzzy human-readable duration or into human-readable size, to your flask 9 | application. 10 | 11 | Contributing 12 | ------------ 13 | 14 | Don't hesitate to create a `GitHub issue 15 | `_ for any **bug** or 16 | **suggestion**. 17 | 18 | """ 19 | import io 20 | import os 21 | import re 22 | from setuptools import ( 23 | find_packages, setup 24 | ) 25 | 26 | 27 | def read(*parts): 28 | """Reads the content of the file created from *parts*.""" 29 | try: 30 | with io.open(os.path.join(*parts), 'r', encoding='utf-8') as f: 31 | return f.readlines() 32 | except IOError: 33 | return [] 34 | 35 | 36 | def get_version(): 37 | version_file = ''.join(read('flask_humanize', '__init__.py')) 38 | version_match = re.search(r'^__version__ = [\'"]([^\'"]*)[\'"]', 39 | version_file, re.MULTILINE) 40 | if version_match: 41 | return version_match.group(1) 42 | raise RuntimeError('Unable to find version string.') 43 | 44 | 45 | __version__ = get_version() 46 | install_requires = read('requirements', 'main.txt') 47 | 48 | 49 | setup( 50 | name='Flask-Humanize', 51 | version=__version__, 52 | 53 | author='Vital Kudzelka', 54 | author_email='vital.kudzelka@gmail.com', 55 | 56 | url="https://github.com/vitalk/flask-humanize", 57 | description='Common humanization utilities for Flask applications.', 58 | download_url='https://github.com/vitalk/flask-humanize/tarball/%s' % __version__, 59 | long_description=__doc__, 60 | license='MIT', 61 | 62 | packages=find_packages(exclude=['tests']), 63 | zip_safe=False, 64 | platforms='any', 65 | install_requires=install_requires, 66 | 67 | keywords='flask humanize datetime', 68 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 69 | classifiers=[ 70 | 'Development Status :: 4 - Beta', 71 | 'Environment :: Plugins', 72 | 'Environment :: Web Environment', 73 | 'Intended Audience :: Developers', 74 | 'License :: OSI Approved :: MIT License', 75 | 'Operating System :: OS Independent', 76 | 'Programming Language :: Python', 77 | 'Programming Language :: Python :: 2', 78 | 'Programming Language :: Python :: 2.6', 79 | 'Programming Language :: Python :: 2.7', 80 | 'Programming Language :: Python :: 3', 81 | 'Programming Language :: Python :: 3.3', 82 | 'Programming Language :: Python :: 3.4', 83 | 'Programming Language :: Python :: Implementation :: CPython', 84 | 'Programming Language :: Python :: Implementation :: PyPy', 85 | 'Framework :: Flask', 86 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 87 | 'Topic :: Software Development :: Libraries :: Python Modules', 88 | ] 89 | ) 90 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import pytest 4 | from datetime import ( 5 | datetime, timedelta 6 | ) 7 | 8 | from flask import ( 9 | Flask, render_template_string 10 | ) 11 | from flask_humanize import Humanize 12 | 13 | 14 | @pytest.fixture 15 | def app(): 16 | app = Flask(__name__) 17 | app.debug = True 18 | app.testing = True 19 | 20 | @app.context_processor 21 | def expose_current_timestamp(): 22 | return { 23 | 'now': datetime.now(), 24 | } 25 | 26 | @app.route('/naturalday') 27 | def naturalday(): 28 | return render_template_string("{{ now|humanize('naturalday') }}") 29 | 30 | @app.route('/naturaltime') 31 | def naturaltime(): 32 | return render_template_string("{{ now|humanize('naturaltime') }}") 33 | 34 | @app.route('/naturaldelta') 35 | def naturaldelta(): 36 | return render_template_string("{{ value|humanize('naturaldelta') }}", 37 | value=timedelta(days=7)) 38 | 39 | return app 40 | 41 | 42 | @pytest.fixture(autouse=True) 43 | def h(request): 44 | if 'app' not in request.fixturenames: 45 | return 46 | 47 | app = request.getfuncargvalue('app') 48 | return Humanize(app) 49 | -------------------------------------------------------------------------------- /tests/test_humanize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import pytest 4 | from datetime import ( 5 | date, datetime, timedelta 6 | ) 7 | 8 | from flask import url_for 9 | from flask_humanize.compat import text_type 10 | import humanize 11 | 12 | 13 | now = datetime.now() 14 | today = date.today() 15 | one_day = timedelta(days=1) 16 | one_month = timedelta(days=31) 17 | birthday = date(1987, 4, 21) 18 | 19 | 20 | def b(string): 21 | return string.encode('utf-8') if isinstance(string, text_type) else string 22 | 23 | 24 | class TestInit: 25 | 26 | def test_register_themself(self, app, h): 27 | assert app.extensions['humanize'] is h 28 | 29 | def test_register_before_request_callback(self, app, h): 30 | assert h._set_locale in app.before_request_funcs[None] 31 | 32 | def test_register_after_request_callback(self, app, h): 33 | assert h._unset_locale in app.after_request_funcs[None] 34 | 35 | def test_register_template_filter(self, app, h): 36 | assert h._humanize in app.jinja_env.filters.values() 37 | 38 | def test_defaults(self, config, h): 39 | assert config['HUMANIZE_DEFAULT_LOCALE'] == 'en' 40 | assert not config['HUMANIZE_USE_UTC'] 41 | 42 | 43 | @pytest.mark.usefixtures('app') 44 | class TestHumanize: 45 | 46 | def test_naturalday(self, h): 47 | assert h._humanize(today, 'naturalday') == 'today' 48 | assert h._humanize(today + one_day, 'naturalday') == 'tomorrow' 49 | assert h._humanize(today - one_day, 'naturalday') == 'yesterday' 50 | 51 | def test_naturaltime(self, h): 52 | assert h._humanize(now) == 'now' 53 | assert h._humanize(now - one_month) == 'a month ago' 54 | 55 | def test_naturaltime_nomonths(self, h): 56 | assert h._humanize(now - one_month, months=False) == '31 days ago' 57 | 58 | def test_naturaldelta(self, h): 59 | assert h._humanize(one_day, 'naturaldelta') == 'a day' 60 | assert h._humanize(one_month, 'naturaldelta') == 'a month' 61 | 62 | def test_naturaldelta_nomonths(self, h): 63 | assert h._humanize(one_month, 'naturaldelta', months=False) == '31 days' 64 | 65 | def test_naturaldate(self, h): 66 | assert h._humanize(birthday, 'naturaldate') == 'Apr 21 1987' 67 | 68 | def test_invalid_fname(self, h): 69 | with pytest.raises(Exception) as exc: 70 | h._humanize(now, fname='foo') 71 | 72 | assert text_type(exc.value) == ( 73 | "Humanize module does not contains function 'foo'" 74 | ) 75 | 76 | def test_unsupported_arguments(self, h): 77 | with pytest.raises(ValueError) as exc: 78 | h._humanize(now, foo=42) 79 | 80 | assert text_type(exc.value) == ( 81 | "An error occured during execution function 'naturaltime'" 82 | ) 83 | 84 | 85 | class TestL10N: 86 | 87 | def test_locale_selector(self, client, h): 88 | @h.localeselector 89 | def get_locale(): 90 | return 'ru_RU' 91 | 92 | assert client.get(url_for('naturalday')).data == b(u'сегодня') 93 | assert client.get(url_for('naturaltime')).data == b(u'сейчас') 94 | assert client.get(url_for('naturaldelta')).data == b(u'7 дней') 95 | 96 | @pytest.mark.options(humanize_default_locale='ru_RU') 97 | def test_default_locale(self, client): 98 | assert client.get(url_for('naturaltime')).data == b(u'сейчас') 99 | 100 | def test_fallback_to_default_if_no_translations_for_locale(self, client, h): 101 | @h.localeselector 102 | def get_locale(): 103 | return 'tlh' 104 | 105 | assert client.get(url_for('naturaltime')).data == b'now' 106 | 107 | 108 | @pytest.mark.usefixtures('app') 109 | class TestUTC: 110 | 111 | @pytest.mark.options(humanize_use_utc=True) 112 | def test_use_utc(self, h): 113 | assert humanize.time._now == datetime.utcnow 114 | 115 | @pytest.mark.options(humanize_use_utc=False) 116 | def test_dont_use_utc(self, h): 117 | assert humanize.time._now == datetime.now 118 | 119 | @pytest.mark.options(humanize_use_utc=True) 120 | def test_naturaltime_with_utc(self, h): 121 | assert h._humanize(datetime.utcnow()) == 'now' 122 | --------------------------------------------------------------------------------