diff options
author | Christopher Lenz <cmlenz@gmail.com> | 2007-05-29 20:33:55 +0000 |
---|---|---|
committer | Christopher Lenz <cmlenz@gmail.com> | 2007-05-29 20:33:55 +0000 |
commit | 00f16f91a878676a55aa1e6e85b5e1092f7cf8e9 (patch) | |
tree | 0471ffecac176459ec2b733353f8d577f582a6b8 | |
parent | 3a6d9c9d0644259b2baa6f5ee5c9ca9fcae1e6a2 (diff) | |
download | babel-00f16f91a878676a55aa1e6e85b5e1092f7cf8e9.tar.gz |
Import of initial code base.
-rw-r--r-- | COPYING | 28 | ||||
-rw-r--r-- | ChangeLog | 1 | ||||
-rw-r--r-- | INSTALL.txt | 37 | ||||
-rw-r--r-- | MANIFEST.in | 3 | ||||
-rw-r--r-- | README.txt | 12 | ||||
-rw-r--r-- | babel/__init__.py | 32 | ||||
-rw-r--r-- | babel/catalog/__init__.py | 72 | ||||
-rw-r--r-- | babel/catalog/extract.py | 214 | ||||
-rw-r--r-- | babel/catalog/frontend.py | 150 | ||||
-rw-r--r-- | babel/catalog/pofile.py | 206 | ||||
-rw-r--r-- | babel/catalog/tests/__init__.py | 24 | ||||
-rw-r--r-- | babel/catalog/tests/extract.py | 25 | ||||
-rw-r--r-- | babel/catalog/tests/pofile.py | 25 | ||||
-rw-r--r-- | babel/core.py | 357 | ||||
-rw-r--r-- | babel/dates.py | 383 | ||||
-rw-r--r-- | babel/numbers.py | 164 | ||||
-rw-r--r-- | babel/tests/__init__.py | 28 | ||||
-rw-r--r-- | babel/tests/core.py | 25 | ||||
-rw-r--r-- | babel/tests/dates.py | 25 | ||||
-rw-r--r-- | babel/tests/numbers.py | 25 | ||||
-rw-r--r-- | babel/tests/util.py | 25 | ||||
-rw-r--r-- | babel/util.py | 183 | ||||
-rwxr-xr-x | scripts/import_cldr.py | 223 | ||||
-rwxr-xr-x | setup.py | 117 |
24 files changed, 2384 insertions, 0 deletions
@@ -0,0 +1,28 @@ +Copyright (C) 2007 Edgewall Software +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. The name of the author may not be used to endorse or promote + products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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/ChangeLog b/ChangeLog new file mode 100644 index 0000000..bb5e1b7 --- /dev/null +++ b/ChangeLog @@ -0,0 +1 @@ +Nothing so far, not even public yet. diff --git a/INSTALL.txt b/INSTALL.txt new file mode 100644 index 0000000..89209f8 --- /dev/null +++ b/INSTALL.txt @@ -0,0 +1,37 @@ +Installing Babel +================ + +Prerequisites +------------- + + * Python 2.3 or later (2.4 or later is recommended) + * Optional: setuptools 0.6b1 or later + + +Installation +------------ + +Once you've downloaded and unpacked a Babel source release, enter the +directory where the archive was unpacked, and run: + + $ python setup.py install + +Note that you may need administrator/root privileges for this step, as +this command will by default attempt to install Babel to the Python +site-packages directory on your system. + +For advanced options, please refer to the easy_install and/or the distutils +documentation: + + http://peak.telecommunity.com/DevCenter/EasyInstall + http://docs.python.org/inst/inst.html + + +Support +------- + +If you encounter any problems with Babel, please don't hesitate to ask +questions on the Babel mailing list or IRC channel: + + http://babel.edgewall.org/wiki/MailingList + http://babel.edgewall.org/wiki/IrcChannel diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..4fa7a34 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include babel/localedata/*.dat +include doc/api/*.* +include doc/*.html diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..9c55b56 --- /dev/null +++ b/README.txt @@ -0,0 +1,12 @@ +About Babel +=========== + +Babel is a Python library that provides an integrated collection of +utilities that assist with internationalizing and localizing Python +applications (in particular web-based applications.) + +Details can be found in the HTML files in the `doc` folder. + +For more information please visit the Babel web site: + + <http://babel.edgewall.org/> diff --git a/babel/__init__.py b/babel/__init__.py new file mode 100644 index 0000000..cca0636 --- /dev/null +++ b/babel/__init__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +"""Integrated collection of utilities that assist in internationalizing and +localizing applications. + +This package is basically composed of two major parts: + + * tools to build and work with ``gettext`` message catalogs + * a Python interface to the CLDR (Common Locale Data Repository), providing + access to various locale display names, localized number and date + formatting, etc. + +:see: http://www.gnu.org/software/gettext/ +:see: http://docs.python.org/lib/module-gettext.html +:see: http://www.unicode.org/cldr/ +""" + +from babel.core import Locale + +__docformat__ = 'restructuredtext en' +__version__ = __import__('pkg_resources').get_distribution('Babel').version diff --git a/babel/catalog/__init__.py b/babel/catalog/__init__.py new file mode 100644 index 0000000..3f99b35 --- /dev/null +++ b/babel/catalog/__init__.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +"""Support for ``gettext`` message catalogs.""" + +import gettext + +__all__ = ['Translations'] + +DEFAULT_DOMAIN = 'messages' + + +class Translations(gettext.GNUTranslations): + """An extended translation catalog class.""" + + def __init__(self, fileobj=None): + """Initialize the translations catalog. + + :param fileobj: the file-like object the translation should be read + from + """ + GNUTranslations.__init__(self, fp=fileobj) + self.files = [getattr(fileobj, 'name')] + + def load(cls, dirname=None, locales=None, domain=DEFAULT_DOMAIN): + """Load translations from the given directory. + + :param dirname: the directory containing the ``MO`` files + :param locales: the list of locales in order of preference (items in + this list can be either `Locale` objects or locale + strings) + :param domain: the message domain + :return: the loaded catalog, or a ``NullTranslations`` instance if no + matching translations were found + :rtype: `Translations` + """ + locales = [str(locale) for locale in locales] + filename = gettext.find(domain, dirname, locales) + if not filename: + return NullTranslations() + return cls(open(filename, 'rb')) + load = classmethod(load) + + def merge(self, translations): + """Merge the given translations into the catalog. + + Message translations in the specfied catalog override any messages with + the same identifier in the existing catalog. + + :param translations: the `Translations` instance with the messages to + merge + :return: the `Translations` instance (``self``) so that `merge` calls + can be easily chained + :rtype: `Translations` + """ + if isinstance(translations, Translations): + self._catalog.update(translations._catalog) + self.files.extend(translations.files) + return self + + def __repr__(self): + return "<%s %r>" % (type(self).__name__) diff --git a/babel/catalog/extract.py b/babel/catalog/extract.py new file mode 100644 index 0000000..8aaa1fe --- /dev/null +++ b/babel/catalog/extract.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +"""Basic infrastructure for extracting localizable messages from source files. + +This module defines an extensible system for collecting localizable message +strings from a variety of sources. A native extractor for Python source files +is builtin, extractors for other sources can be added using very simple plugins. + +The main entry points into the extraction functionality are the functions +`extract_from_dir` and `extract_from_file`. +""" + +import os +from pkg_resources import working_set +import sys +from tokenize import generate_tokens, NAME, OP, STRING + +from babel.util import extended_glob + +__all__ = ['extract', 'extract_from_dir', 'extract_from_file'] +__docformat__ = 'restructuredtext en' + +GROUP_NAME = 'babel.extractors' + +KEYWORDS = ( + '_', 'gettext', 'ngettext', + 'dgettext', 'dngettext', + 'ugettext', 'ungettext' +) + +DEFAULT_MAPPING = { + 'genshi': ['*.html', '**/*.html'], + 'python': ['*.py', '**/*.py'] +} + +def extract_from_dir(dirname, mapping=DEFAULT_MAPPING, keywords=KEYWORDS, + options=None): + """Extract messages from any source files found in the given directory. + + This function generates tuples of the form: + + ``(filename, lineno, funcname, message)`` + + Which extraction method used is per file is determined by the `mapping` + parameter, which maps extraction method names to lists of extended glob + patterns. For example, the following is the default mapping: + + >>> mapping = { + ... 'python': ['*.py', '**/*.py'] + ... } + + This basically says that files with the filename extension ".py" at any + level inside the directory should be processed by the "python" extraction + method. Files that don't match any of the patterns are ignored. + + The following extended mapping would also use the "genshi" extraction method + on any file in "templates" subdirectory: + + >>> mapping = { + ... 'genshi': ['**/templates/*.*', '**/templates/**/*.*'], + ... 'python': ['*.py', '**/*.py'] + ... } + + :param dirname: the path to the directory to extract messages from + :param mapping: a mapping of extraction method names to extended glob + patterns + :param keywords: a list of keywords (i.e. function names) that should be + recognized as translation functions + :param options: a dictionary of additional options (optional) + :return: an iterator over ``(filename, lineno, funcname, message)`` tuples + :rtype: ``iterator`` + """ + extracted_files = {} + for method, patterns in mapping.items(): + for pattern in patterns: + for filename in extended_glob(pattern, dirname): + if filename in extracted_files: + continue + filepath = os.path.join(dirname, filename) + for line, func, key in extract_from_file(method, filepath, + keywords=keywords, + options=options): + yield filename, line, func, key + extracted_files[filename] = True + +def extract_from_file(method, filename, keywords=KEYWORDS, options=None): + """Extract messages from a specific file. + + This function returns a list of tuples of the form: + + ``(lineno, funcname, message)`` + + :param filename: the path to the file to extract messages from + :param method: a string specifying the extraction method (.e.g. "python") + :param keywords: a list of keywords (i.e. function names) that should be + recognized as translation functions + :param options: a dictionary of additional options (optional) + :return: the list of extracted messages + :rtype: `list` + """ + fileobj = open(filename, 'U') + try: + return list(extract(method, fileobj, keywords, options=options)) + finally: + fileobj.close() + +def extract(method, fileobj, keywords=KEYWORDS, options=None): + """Extract messages from the given file-like object using the specified + extraction method. + + This function returns a list of tuples of the form: + + ``(lineno, funcname, message)`` + + The implementation dispatches the actual extraction to plugins, based on the + value of the ``method`` parameter. + + >>> source = '''# foo module + ... def run(argv): + ... print _('Hello, world!') + ... ''' + + >>> from StringIO import StringIO + >>> for message in extract('python', StringIO(source)): + ... print message + (3, '_', 'Hello, world!') + + :param method: a string specifying the extraction method (.e.g. "python") + :param fileobj: the file-like object the messages should be extracted from + :param keywords: a list of keywords (i.e. function names) that should be + recognized as translation functions + :param options: a dictionary of additional options (optional) + :return: the list of extracted messages + :rtype: `list` + :raise ValueError: if the extraction method is not registered + """ + for entry_point in working_set.iter_entry_points(GROUP_NAME, method): + func = entry_point.load(require=True) + return list(func(fileobj, keywords, options=options or {})) + raise ValueError('Unknown extraction method %r' % method) + +def extract_genshi(fileobj, keywords, options): + """Extract messages from Genshi templates. + + :param fileobj: the file-like object the messages should be extracted from + :param keywords: a list of keywords (i.e. function names) that should be + recognized as translation functions + :param options: a dictionary of additional options (optional) + :return: an iterator over ``(lineno, funcname, message)`` tuples + :rtype: ``iterator`` + """ + from genshi.filters.i18n import Translator + from genshi.template import MarkupTemplate + tmpl = MarkupTemplate(fileobj, filename=getattr(fileobj, 'name')) + translator = Translator(None) + for message in translator.extract(tmpl.stream, gettext_functions=keywords): + yield message + +def extract_python(fileobj, keywords, options): + """Extract messages from Python source code. + + :param fileobj: the file-like object the messages should be extracted from + :param keywords: a list of keywords (i.e. function names) that should be + recognized as translation functions + :param options: a dictionary of additional options (optional) + :return: an iterator over ``(lineno, funcname, message)`` tuples + :rtype: ``iterator`` + """ + funcname = None + lineno = None + buf = [] + messages = [] + in_args = False + + tokens = generate_tokens(fileobj.readline) + for tok, value, (lineno, _), _, _ in tokens: + if funcname and tok == OP and value == '(': + in_args = True + elif funcname and in_args: + if tok == OP and value == ')': + in_args = False + if buf: + messages.append(''.join(buf)) + del buf[:] + if filter(None, messages): + if len(messages) > 1: + messages = tuple(messages) + else: + messages = messages[0] + yield lineno, funcname, messages + funcname = lineno = None + messages = [] + elif tok == STRING: + if lineno is None: + lineno = stup[0] + buf.append(value[1:-1]) + elif tok == OP and value == ',': + messages.append(''.join(buf)) + del buf[:] + elif funcname: + funcname = None + elif tok == NAME and value in keywords: + funcname = value diff --git a/babel/catalog/frontend.py b/babel/catalog/frontend.py new file mode 100644 index 0000000..31074d2 --- /dev/null +++ b/babel/catalog/frontend.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +"""Frontends for the message extraction functionality.""" + +from distutils import log +from distutils.cmd import Command +from optparse import OptionParser +import os +import sys + +from babel import __version__ as VERSION +from babel.catalog.extract import extract_from_dir, KEYWORDS +from babel.catalog.pofile import write_po + +__all__ = ['extract_messages', 'main'] +__docformat__ = 'restructuredtext en' + + +class extract_messages(Command): + """Message extraction command for use in ``setup.py`` scripts. + + If correctly installed, this command is available to Setuptools-using + setup scripts automatically. For projects using plain old ``distutils``, + the command needs to be registered explicitly in ``setup.py``:: + + from babel.catalog.frontend import extract_messages + + setup( + ... + cmdclass = {'extract_messages': extract_messages} + ) + + :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_ + :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_ + """ + + description = 'extract localizable strings from the project code' + user_options = [ + ('charset=', None, + 'charset to use in the output file'), + ('keywords=', 'k', + 'comma-separated list of keywords to look for in addition to the ' + 'defaults'), + ('no-location', None, + 'do not include location comments with filename and line number'), + ('omit-header', None, + 'do not include msgid "" entry in header'), + ('output-file=', None, + 'name of the output file'), + ] + boolean_options = ['no-location', 'omit-header'] + + def initialize_options(self): + self.charset = 'utf-8' + self.keywords = KEYWORDS + self.no_location = False + self.omit_header = False + self.output_file = None + self.input_dirs = None + + def finalize_options(self): + if not self.input_dirs: + self.input_dirs = dict.fromkeys([k.split('.',1)[0] + for k in self.distribution.packages + ]).keys() + if isinstance(self.keywords, basestring): + new_keywords = [k.strip() for k in self.keywords.split(',')] + self.keywords = list(KEYWORDS) + new_keywords + + def run(self): + outfile = open(self.output_file, 'w') + try: + messages = [] + for dirname in self.input_dirs: + log.info('extracting messages from %r' % dirname) + extracted = extract_from_dir(dirname, keywords=self.keywords) + for filename, lineno, funcname, message in extracted: + messages.append((os.path.join(dirname, filename), lineno, + funcname, message)) + write_po(outfile, messages, charset=self.charset, + no_location=self.no_location, omit_header=self.omit_header) + log.info('writing PO file to %s' % self.output_file) + finally: + outfile.close() + + +def main(argv=sys.argv): + """Command-line interface. + + This function provides a simple command-line interface to the message + extraction and PO file generation functionality. + + :param argv: list of arguments passed on the command-line + """ + parser = OptionParser(usage='%prog [options] dirname1 <dirname2> ...', + version='%%prog %s' % VERSION) + parser.add_option('--charset', dest='charset', default='utf-8', + help='charset to use in the output') + parser.add_option('-k', '--keyword', dest='keywords', + default=list(KEYWORDS), action='append', + help='keywords to look for in addition to the defaults. ' + 'You can specify multiple -k flags on the command ' + 'line.') + parser.add_option('--no-location', dest='no_location', default=False, + action='store_true', + help='do not include location comments with filename and ' + 'line number') + parser.add_option('--omit-header', dest='omit_header', default=False, + action='store_true', + help='do not include msgid "" entry in header') + parser.add_option('-o', '--output', dest='output', + help='path to the output POT file') + options, args = parser.parse_args(argv[1:]) + if not args: + parser.error('incorrect number of arguments') + + if options.output not in (None, '-'): + outfile = open(options.output, 'w') + else: + outfile = sys.stdout + + try: + messages = [] + for dirname in args: + if not os.path.isdir(dirname): + parser.error('%r is not a directory' % dirname) + extracted = extract_from_dir(dirname, keywords=options.keywords) + for filename, lineno, funcname, message in extracted: + messages.append((os.path.join(dirname, filename), lineno, + funcname, message)) + write_po(outfile, messages, + charset=options.charset, no_location=options.no_location, + omit_header=options.omit_header) + finally: + if options.output: + outfile.close() + +if __name__ == '__main__': + main() diff --git a/babel/catalog/pofile.py b/babel/catalog/pofile.py new file mode 100644 index 0000000..595641e --- /dev/null +++ b/babel/catalog/pofile.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +"""Reading and writing of files in the ``gettext`` PO (portable object) +format. + +:see: `The Format of PO Files + <http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files>`_ +""" + +# TODO: line wrapping + +from datetime import datetime +import re + +from babel import __version__ as VERSION + +__all__ = ['escape', 'normalize', 'read_po', 'write_po'] + +POT_HEADER = """\ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR ORGANIZATION +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: %%(project)s %%(version)s\\n" +"POT-Creation-Date: %%(time)s\\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n" +"Language-Team: LANGUAGE <LL@li.org>\\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=%%(charset)s\\n" +"Content-Transfer-Encoding: %%(charset)s\\n" +"Generated-By: Babel %s\\n" + +""" % VERSION + +PYTHON_FORMAT = re.compile(r'(\%\(([\w]+)\)[diouxXeEfFgGcrs])').search + +def escape(string): + r"""Escape the given string so that it can be included in double-quoted + strings in ``PO`` files. + + >>> escape('''Say: + ... "hello, world!" + ... ''') + 'Say:\\n \\"hello, world!\\"\\n' + + :param string: the string to escape + :return: the escaped string + :rtype: `str` or `unicode` + """ + return string.replace('\\', '\\\\') \ + .replace('\t', '\\t') \ + .replace('\r', '\\r') \ + .replace('\n', '\\n') \ + .replace('\"', '\\"') + +def normalize(string, charset='utf-8'): + """This converts a string into a format that is appropriate for .po files, + namely much closer to C style. + + :param string: the string to normalize + :param charset: the encoding to use for `unicode` strings + :return: the normalized string + :rtype: `str` + """ + string = string.encode(charset, 'backslashreplace') + lines = string.split('\n') + if len(lines) == 1: + string = '"' + escape(string) + '"' + else: + if not lines[-1]: + del lines[-1] + lines[-1] = lines[-1] + '\n' + for i in range(len(lines)): + lines[i] = escape(lines[i]) + lineterm = '\\n"\n"' + string = '""\n"' + lineterm.join(lines) + '"' + return string + +def read_po(fileobj): + """Parse a PO file. + + This function yields tuples of the form: + + ``(message, translation, locations)`` + + where: + + * ``message`` is the original (untranslated) message, or a + ``(singular, plural)`` tuple for pluralizable messages + * ``translation`` is the translation of the message, or a tuple of + translations for pluralizable messages + * ``locations`` is a sequence of ``(filename, lineno)`` tuples + + :param fileobj: the file-like object to read the PO file from + :return: an iterator over ``(message, translation, location)`` tuples + :rtype: ``iterator`` + """ + for line in fileobj.readlines(): + line = line.strip() + if line.startswith('#'): + continue # TODO: process comments + else: + if line.startswith('msgid_plural'): + msg = line[12:].lstrip() + elif line.startswith('msgid'): + msg = line[5:].lstrip() + elif line.startswith('msgstr'): + msg = line[6:].lstrip() + if msg.startswith('['): + pass # plural + +def write_po(fileobj, messages, project=None, version=None, creation_date=None, + charset='utf-8', no_location=False, omit_header=False): + r"""Write a ``gettext`` PO (portable object) file to the given file-like + object. + + The `messages` parameter is expected to be an iterable object producing + tuples of the form: + + ``(filename, lineno, funcname, message)`` + + >>> from StringIO import StringIO + >>> buf = StringIO() + >>> write_po(buf, [ + ... ('main.py', 1, None, u'foo'), + ... ('main.py', 3, 'ngettext', (u'bar', u'baz')) + ... ], omit_header=True) + + >>> print buf.getvalue() + #: main.py:1 + msgid "foo" + msgstr "" + <BLANKLINE> + #: main.py:3 + msgid "bar" + msgid_plural "baz" + msgstr[0] "" + msgstr[1] "" + <BLANKLINE> + <BLANKLINE> + + :param fileobj: the file-like object to write to + :param messages: an iterable over the messages + :param project: the project name + :param version: the project version + :param charset: the encoding + :param no_location: do not emit a location comment for every message + :param omit_header: do not include the ``msgid ""`` entry at the top of the + output + """ + def _normalize(key): + return normalize(key, charset=charset) + + if creation_date is None: + creation_date = datetime.now() + + if not omit_header: + fileobj.write(POT_HEADER % { + 'charset': charset, + 'time': creation_date.strftime('%Y-%m-%d %H:%M'), + 'project': project, + 'version': version + }) + + locations = {} + msgids = [] + + for filename, lineno, funcname, key in messages: + if key in msgids: + locations[key].append((filename, lineno)) + else: + locations[key] = [(filename, lineno)] + msgids.append(key) + + for msgid in msgids: + if not no_location: + for filename, lineno in locations[msgid]: + fileobj.write('#: %s:%s\n' % (filename, lineno)) + if type(msgid) is tuple: + assert len(msgid) == 2 + if PYTHON_FORMAT(msgid[0]) or PYTHON_FORMAT(msgid[1]): + fileobj.write('#, python-format\n') + fileobj.write('msgid %s\n' % normalize(msgid[0], charset)) + fileobj.write('msgid_plural %s\n' % normalize(msgid[1], charset)) + fileobj.write('msgstr[0] ""\n') + fileobj.write('msgstr[1] ""\n') + else: + if PYTHON_FORMAT(msgid): + fileobj.write('#, python-format\n') + fileobj.write('msgid %s\n' % normalize(msgid, charset)) + fileobj.write('msgstr ""\n') + fileobj.write('\n') diff --git a/babel/catalog/tests/__init__.py b/babel/catalog/tests/__init__.py new file mode 100644 index 0000000..a371f64 --- /dev/null +++ b/babel/catalog/tests/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import unittest + +def suite(): + from babel.catalog.tests import extract, pofile + suite = unittest.TestSuite() + suite.addTest(extract.suite()) + suite.addTest(pofile.suite()) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/babel/catalog/tests/extract.py b/babel/catalog/tests/extract.py new file mode 100644 index 0000000..8c9fcfb --- /dev/null +++ b/babel/catalog/tests/extract.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import doctest +import unittest + +from babel.catalog import extract + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(extract)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/babel/catalog/tests/pofile.py b/babel/catalog/tests/pofile.py new file mode 100644 index 0000000..c8958e0 --- /dev/null +++ b/babel/catalog/tests/pofile.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import doctest +import unittest + +from babel.catalog import pofile + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(pofile)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/babel/core.py b/babel/core.py new file mode 100644 index 0000000..607ea70 --- /dev/null +++ b/babel/core.py @@ -0,0 +1,357 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +"""Core locale representation and locale data access gateway.""" + +import pickle +from pkg_resources import resource_filename +try: + import threading +except ImportError: + import dummy_threading as threading + +__all__ = ['Locale', 'negotiate', 'parse'] +__docformat__ = 'restructuredtext en' + + +class Locale(object): + """Representation of a specific locale. + + >>> locale = Locale('en', territory='US') + >>> repr(locale) + '<Locale "en_US">' + >>> locale.display_name + u'English (United States)' + + A `Locale` object can also be instantiated from a raw locale string: + + >>> locale = Locale.parse('en-US', sep='-') + >>> repr(locale) + '<Locale "en_US">' + + `Locale` objects provide access to a collection of locale data, such as + territory and language names, number and date format patterns, and more: + + >>> locale.number_symbols['decimal'] + u'.' + + :see: `IETF RFC 3066 <http://www.ietf.org/rfc/rfc3066.txt>`_ + """ + _cache = {} + _cache_lock = threading.Lock() + + def __new__(cls, language, territory=None, variant=None): + """Create new locale object, or load it from the cache if it had already + been instantiated. + + >>> l1 = Locale('en') + >>> l2 = Locale('en') + >>> l1 is l2 + True + + :param language: the language code + :param territory: the territory (country or region) code + :param variant: the variant code + :return: new or existing `Locale` instance + :rtype: `Locale` + """ + key = (language, territory, variant) + cls._cache_lock.acquire() + try: + self = cls._cache.get(key) + if self is None: + self = super(Locale, cls).__new__(cls, language, territory, + variant) + cls._cache[key] = self + return self + finally: + self._cache_lock.release() + + def __init__(self, language, territory=None, variant=None): + """Initialize the locale object from the given identifier components. + + >>> locale = Locale('en', 'US') + >>> locale.language + 'en' + >>> locale.territory + 'US' + + :param language: the language code + :param territory: the territory (country or region) code + :param variant: the variant code + """ + self.language = language + self.territory = territory + self.variant = variant + self.__data = None + + def parse(cls, identifier, sep='_'): + """Create a `Locale` instance for the given locale identifier. + + >>> l = Locale.parse('de-DE', sep='-') + >>> l.display_name + u'Deutsch (Deutschland)' + + If the `identifier` parameter is not a string, but actually a `Locale` + object, that object is returned: + + >>> Locale.parse(l) + <Locale "de_DE"> + + :param identifier: the locale identifier string + :param sep: optional component separator + :return: a corresponding `Locale` instance + :rtype: `Locale` + :raise `ValueError`: if the string does not appear to be a valid locale + identifier + """ + if type(identifier) is cls: + return identifier + return cls(*parse(identifier, sep=sep)) + parse = classmethod(parse) + + def __repr__(self): + return '<Locale "%s">' % str(self) + + def __str__(self): + return '_'.join(filter(None, [self.language, self.territory, + self.variant])) + + def _data(self): + if self.__data is None: + filename = resource_filename(__name__, 'localedata/%s.dat' % self) + fileobj = open(filename, 'rb') + try: + self.__data = pickle.load(fileobj) + finally: + fileobj.close() + return self.__data + _data = property(_data) + + def display_name(self): + retval = self.languages.get(self.language) + if self.territory: + variant = '' + if self.variant: + variant = ', %s' % self.variants.get(self.variant) + retval += ' (%s%s)' % (self.territories.get(self.territory), variant) + return retval + display_name = property(display_name, doc="""\ + The localized display name of the locale. + + >>> Locale('en').display_name + u'English' + >>> Locale('en', 'US').display_name + u'English (United States)' + + :type: `unicode` + """) + + def languages(self): + return self._data['languages'] + languages = property(languages, doc="""\ + Mapping of language codes to translated language names. + + >>> Locale('de', 'DE').languages['ja'] + u'Japanisch' + + :type: `dict` + :see: `ISO 639 <http://www.loc.gov/standards/iso639-2/>`_ + """) + + def scripts(self): + return self._data['scripts'] + scripts = property(scripts, doc="""\ + Mapping of script codes to translated script names. + + >>> Locale('en', 'US').scripts['Hira'] + u'Hiragana' + + :type: `dict` + :see: `ISO 15924 <http://www.evertype.com/standards/iso15924/>`_ + """) + + def territories(self): + return self._data['territories'] + territories = property(territories, doc="""\ + Mapping of script codes to translated script names. + + >>> Locale('es', 'CO').territories['DE'] + u'Alemania' + + :type: `dict` + :see: `ISO 3166 <http://www.iso.org/iso/en/prods-services/iso3166ma/>`_ + """) + + def variants(self): + return self._data['variants'] + variants = property(variants, doc="""\ + Mapping of script codes to translated script names. + + >>> Locale('de', 'DE').variants['1901'] + u'alte deutsche Rechtschreibung' + + :type: `dict` + """) + + def number_symbols(self): + return self._data['number_symbols'] + number_symbols = property(number_symbols, doc="""\ + Symbols used in number formatting. + + >>> Locale('fr', 'FR').number_symbols['decimal'] + u',' + + :type: `dict` + """) + + def periods(self): + return self._data['periods'] + periods = property(periods, doc="""\ + Locale display names for day periods (AM/PM). + + >>> Locale('en', 'US').periods['am'] + u'AM' + + :type: `dict` + """) + + def days(self): + return self._data['days'] + days = property(days, doc="""\ + Locale display names for weekdays. + + >>> Locale('de', 'DE').days['format']['wide'][4] + u'Donnerstag' + + :type: `dict` + """) + + def months(self): + return self._data['months'] + months = property(months, doc="""\ + Locale display names for months. + + >>> Locale('de', 'DE').months['format']['wide'][10] + u'Oktober' + + :type: `dict` + """) + + def quarters(self): + return self._data['quarters'] + quarters = property(quarters, doc="""\ + Locale display names for quarters. + + >>> Locale('de', 'DE').quarters['format']['wide'][1] + u'1. Quartal' + + :type: `dict` + """) + + def eras(self): + return self._data['eras'] + eras = property(eras, doc="""\ + Locale display names for eras. + + >>> Locale('en', 'US').eras['wide'][1] + u'Anno Domini' + >>> Locale('en', 'US').eras['abbreviated'][0] + u'BC' + + :type: `dict` + """) + + def date_formats(self): + return self._data['date_formats'] + date_formats = property(date_formats, doc="""\ + Locale patterns for date formatting. + + >>> Locale('en', 'US').date_formats['short'] + <DateTimeFormatPattern u'M/d/yy'> + >>> Locale('fr', 'FR').date_formats['long'] + <DateTimeFormatPattern u'd MMMM yyyy'> + + :type: `dict` + """) + + def time_formats(self): + return self._data['time_formats'] + time_formats = property(time_formats, doc="""\ + Locale patterns for time formatting. + + >>> Locale('en', 'US').time_formats['short'] + <DateTimeFormatPattern u'h:mm a'> + >>> Locale('fr', 'FR').time_formats['long'] + <DateTimeFormatPattern u'HH:mm:ss z'> + + :type: `dict` + """) + + +def negotiate(preferred, available): + """Find the best match between available and requested locale strings. + + >>> negotiate(['de_DE', 'en_US'], ['de_DE', 'de_AT']) + 'de_DE' + >>> negotiate(['de_DE', 'en_US'], ['en', 'de']) + 'de' + + :param preferred: the list of locale strings preferred by the user + :param available: the list of locale strings available + :return: the locale identifier for the best match, or `None` if no match + was found + :rtype: `str` + """ + for locale in preferred: + if locale in available: + return locale + parts = locale.split('_') + if len(parts) > 1 and parts[0] in available: + return parts[0] + return None + +def parse(identifier, sep='_'): + """Parse a locale identifier into a ``(language, territory, variant)`` + tuple. + + >>> parse('zh_CN') + ('zh', 'CN', None) + + The default component separator is "_", but a different separator can be + specified using the `sep` parameter: + + >>> parse('zh-CN', sep='-') + ('zh', 'CN', None) + + :param identifier: the locale identifier string + :param sep: character that separates the different parts of the locale + string + :return: the ``(language, territory, variant)`` tuple + :rtype: `tuple` + :raise `ValueError`: if the string does not appear to be a valid locale + identifier + + :see: `IETF RFC 3066 <http://www.ietf.org/rfc/rfc3066.txt>`_ + """ + parts = identifier.split(sep) + lang, territory, variant = parts[0].lower(), None, None + if not lang.isalpha(): + raise ValueError('expected only letters, got %r' % lang) + if len(parts) > 1: + territory = parts[1].upper().split('.', 1)[0] + if not territory.isalpha(): + raise ValueError('expected only letters, got %r' % territory) + if len(parts) > 2: + variant = parts[2].upper().split('.', 1)[0] + return lang, territory, variant diff --git a/babel/dates.py b/babel/dates.py new file mode 100644 index 0000000..4c6406d --- /dev/null +++ b/babel/dates.py @@ -0,0 +1,383 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +"""Locale dependent formatting and parsing of dates and times. + +The default locale for the functions in this module is determined by the +following environment variables, in that order: + + * ``LC_TIME``, + * ``LC_ALL``, and + * ``LANG`` +""" + +from datetime import date, datetime, time + +from babel.core import Locale +from babel.util import default_locale + +__all__ = ['format_date', 'format_datetime', 'format_time', 'parse_date', + 'parse_datetime', 'parse_time'] +__docformat__ = 'restructuredtext en' + +LC_TIME = default_locale('LC_TIME') + +def get_period_names(locale=LC_TIME): + """Return the names for day periods (AM/PM) used by the locale. + + >>> get_period_names(locale='en_US')['am'] + u'AM' + + :param locale: the `Locale` object, or a locale string + :return: the dictionary of period names + :rtype: `dict` + """ + return Locale.parse(locale).periods + +def get_day_names(width='wide', context='format', locale=LC_TIME): + """Return the day names used by the locale for the specified format. + + >>> get_day_names('wide', locale='en_US')[1] + u'Monday' + >>> get_day_names('abbreviated', locale='es')[1] + u'lun' + >>> get_day_names('narrow', context='stand-alone', locale='de_DE')[1] + u'M' + + :param width: the width to use, one of "wide", "abbreviated", or "narrow" + :param context: the context, either "format" or "stand-alone" + :param locale: the `Locale` object, or a locale string + :return: the dictionary of day names + :rtype: `dict` + """ + return Locale.parse(locale).days[context][width] + +def get_month_names(width='wide', context='format', locale=LC_TIME): + """Return the month names used by the locale for the specified format. + + >>> get_month_names('wide', locale='en_US')[1] + u'January' + >>> get_month_names('abbreviated', locale='es')[1] + u'ene' + >>> get_month_names('narrow', context='stand-alone', locale='de_DE')[1] + u'J' + + :param width: the width to use, one of "wide", "abbreviated", or "narrow" + :param context: the context, either "format" or "stand-alone" + :param locale: the `Locale` object, or a locale string + :return: the dictionary of month names + :rtype: `dict` + """ + return Locale.parse(locale).months[context][width] + +def get_quarter_names(width='wide', context='format', locale=LC_TIME): + """Return the quarter names used by the locale for the specified format. + + >>> get_quarter_names('wide', locale='en_US')[1] + u'1st quarter' + >>> get_quarter_names('abbreviated', locale='de_DE')[1] + u'Q1' + + :param width: the width to use, one of "wide", "abbreviated", or "narrow" + :param context: the context, either "format" or "stand-alone" + :param locale: the `Locale` object, or a locale string + :return: the dictionary of quarter names + :rtype: `dict` + """ + return Locale.parse(locale).quarters[context][width] + +def get_era_names(width='wide', locale=LC_TIME): + """Return the era names used by the locale for the specified format. + + >>> get_era_names('wide', locale='en_US')[1] + u'Anno Domini' + >>> get_era_names('abbreviated', locale='de_DE')[1] + u'n. Chr.' + + :param width: the width to use, either "wide" or "abbreviated" + :param locale: the `Locale` object, or a locale string + :return: the dictionary of era names + :rtype: `dict` + """ + return Locale.parse(locale).eras[width] + +def get_date_format(format='medium', locale=LC_TIME): + """Return the date formatting patterns used by the locale for the specified + format. + + >>> get_date_format(locale='en_US') + <DateTimeFormatPattern u'MMM d, yyyy'> + >>> get_date_format('full', locale='de_DE') + <DateTimeFormatPattern u'EEEE, d. MMMM yyyy'> + + :param format: the format to use, one of "full", "long", "medium", or + "short" + :param locale: the `Locale` object, or a locale string + :return: the date format pattern + :rtype: `dict` + """ + return Locale.parse(locale).date_formats[format] + +def get_time_format(format='medium', locale=LC_TIME): + """Return the time formatting patterns used by the locale for the specified + format. + + >>> get_time_format(locale='en_US') + <DateTimeFormatPattern u'h:mm:ss a'> + >>> get_time_format('full', locale='de_DE') + <DateTimeFormatPattern u"H:mm' Uhr 'z"> + + :param format: the format to use, one of "full", "long", "medium", or + "short" + :param locale: the `Locale` object, or a locale string + :return: the time format pattern + :rtype: `dict` + """ + return Locale.parse(locale).time_formats[format] + +def format_date(date, format='medium', locale=LC_TIME): + """Returns a date formatted according to the given pattern. + + >>> d = date(2007, 04, 01) + >>> format_date(d, locale='en_US') + u'Apr 1, 2007' + >>> format_date(d, format='full', locale='de_DE') + u'Sonntag, 1. April 2007' + + :param date: the ``date`` object + :param format: one of "full", "long", "medium", or "short" + :param locale: a `Locale` object or a locale string + :rtype: `unicode` + """ + locale = Locale.parse(locale) + if format in ('full', 'long', 'medium', 'short'): + format = get_date_format(format, locale=locale) + pattern = parse_pattern(format) + return parse_pattern(format).apply(date, locale) + +def format_datetime(datetime, format='medium', locale=LC_TIME): + """Returns a date formatted according to the given pattern. + + :param datetime: the ``date`` object + :param format: one of "full", "long", "medium", or "short" + :param locale: a `Locale` object or a locale string + :rtype: `unicode` + """ + raise NotImplementedError + +def format_time(time, format='medium', locale=LC_TIME): + """Returns a time formatted according to the given pattern. + + >>> t = time(15, 30) + >>> format_time(t, locale='en_US') + u'3:30:00 PM' + >>> format_time(t, format='short', locale='de_DE') + u'15:30' + + :param time: the ``time`` object + :param format: one of "full", "long", "medium", or "short" + :param locale: a `Locale` object or a locale string + :rtype: `unicode` + """ + locale = Locale.parse(locale) + if format in ('full', 'long', 'medium', 'short'): + format = get_time_format(format, locale=locale) + return parse_pattern(format).apply(time, locale) + +def parse_date(string, locale=LC_TIME): + raise NotImplementedError + +def parse_datetime(string, locale=LC_TIME): + raise NotImplementedError + +def parse_time(string, locale=LC_TIME): + raise NotImplementedError + + +class DateTimeFormatPattern(object): + + def __init__(self, pattern, format): + self.pattern = pattern + self.format = format + + def __repr__(self): + return '<%s %r>' % (type(self).__name__, self.pattern) + + def __unicode__(self): + return self.pattern + + def __mod__(self, other): + assert type(other) is DateTimeFormat + return self.format % other + + def apply(self, datetime, locale): + return self % DateTimeFormat(datetime, locale) + + +class DateTimeFormat(object): + + def __init__(self, value, locale): + assert isinstance(value, (date, datetime, time)) + self.value = value + self.locale = Locale.parse(locale) + + def __getitem__(self, name): + # TODO: a number of fields missing here + if name[0] == 'G': + return self.format_era(len(name)) + elif name[0] == 'y': + return self.format_year(self.value.year, len(name)) + elif name[0] == 'Y': + return self.format_year(self.value.isocalendar()[0], len(name)) + elif name[0] == 'Q': + return self.format_quarter(len(name)) + elif name[0] == 'q': + return self.format_quarter(len(name), context='stand-alone') + elif name[0] == 'M': + return self.format_month(len(name)) + elif name[0] == 'L': + return self.format_month(len(name), context='stand-alone') + elif name[0] == 'd': + return self.format(self.value.day, len(name)) + elif name[0] == 'E': + return self.format_weekday(len(name)) + elif name[0] == 'c': + return self.format_weekday(len(name), context='stand-alone') + elif name[0] == 'a': + return self.format_period() + elif name[0] == 'h': + return self.format(self.value.hour % 12, len(name)) + elif name[0] == 'H': + return self.format(self.value.hour, len(name)) + elif name[0] == 'm': + return self.format(self.value.minute, len(name)) + elif name[0] == 's': + return self.format(self.value.second, len(name)) + else: + raise KeyError('Unsupported date/time field %r' % name[0]) + + def format_era(self, num): + width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)] + era = int(self.value.year >= 0) + return get_era_names(width, self.locale)[era] + + def format_year(self, value, num): + year = self.format(value, num) + if num == 2: + year = year[-2:] + return year + + def format_month(self, num, context='format'): + if num <= 2: + return ('%%0%dd' % num) % self.value.month + width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] + return get_month_names(width, context, self.locale)[self.value.month] + + def format_weekday(self, num, context='format'): + width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)] + weekday = self.value.weekday() + 1 + return get_day_names(width, context, self.locale)[weekday] + + def format_period(self): + period = {0: 'am', 1: 'pm'}[int(self.value.hour > 12)] + return get_period_names(locale=self.locale)[period] + + def format(self, value, length): + return ('%%0%dd' % length) % value + + +PATTERN_CHARS = { + 'G': 5, # era + 'y': None, 'Y': None, 'u': None, # year + 'Q': 4, 'q': 4, # quarter + 'M': 5, 'L': 5, # month + 'w': 2, 'W': 1, # week + 'd': 2, 'D': 3, 'F': 1, 'g': None, # day + 'E': 5, 'e': 5, 'c': 5, # week day + 'a': 1, # period + 'h': 2, 'H': 2, 'K': 2, 'k': 2, # hour + 'm': 2, # minute + 's': 2, 'S': None, 'A': None, # second + 'z': 4, 'Z': 4, 'v': 4 # zone +} + +def parse_pattern(pattern): + """Parse date, time, and datetime format patterns. + + >>> parse_pattern("MMMMd").format + u'%(MMMM)s%(d)s' + >>> parse_pattern("MMM d, yyyy").format + u'%(MMM)s %(d)s, %(yyyy)s' + >>> parse_pattern("H:mm' Uhr 'z").format + u'%(H)s:%(mm)s Uhr %(z)s' + + :param pattern: the formatting pattern to parse + """ + if type(pattern) is DateTimeFormatPattern: + return pattern + + result = [] + quotebuf = None + charbuf = [] + fieldchar = [''] + fieldnum = [0] + + def append_chars(): + result.append(''.join(charbuf).replace('%', '%%')) + del charbuf[:] + + def append_field(): + limit = PATTERN_CHARS[fieldchar[0]] + if limit is not None and fieldnum[0] > limit: + raise ValueError('Invalid length for field: %r' + % (fieldchar[0] * fieldnum[0])) + result.append('%%(%s)s' % (fieldchar[0] * fieldnum[0])) + fieldchar[0] = '' + fieldnum[0] = 0 + + for idx, char in enumerate(pattern): + if quotebuf is None: + if char == "'": # quote started + if fieldchar[0]: + append_field() + elif charbuf: + append_chars() + quotebuf = [] + elif char in PATTERN_CHARS: + if charbuf: + append_chars() + if char == fieldchar[0]: + fieldnum[0] += 1 + else: + if fieldchar[0]: + append_field() + fieldchar[0] = char + fieldnum[0] = 1 + else: + if fieldchar[0]: + append_field() + charbuf.append(char) + + elif quotebuf is not None: + if char == "'": # quote ended + charbuf.extend(quotebuf) + quotebuf = None + else: # inside quote + quotebuf.append(char) + + if fieldchar[0]: + append_field() + elif charbuf: + append_chars() + + return DateTimeFormatPattern(pattern, u''.join(result)) diff --git a/babel/numbers.py b/babel/numbers.py new file mode 100644 index 0000000..ec9f83e --- /dev/null +++ b/babel/numbers.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +"""Locale dependent formatting and parsing of numeric data. + +The default locale for the functions in this module is determined by the +following environment variables, in that order: + + * ``LC_NUMERIC``, + * ``LC_ALL``, and + * ``LANG`` +""" +# TODO: percent and scientific formatting + +import re + +from babel.core import Locale +from babel.util import default_locale + +__all__ = ['format_number', 'format_decimal', 'format_currency', + 'format_percent', 'format_scientific', 'parse_number', + 'parse_decimal'] +__docformat__ = 'restructuredtext en' + +LC_NUMERIC = default_locale('LC_NUMERIC') + +def get_decimal_symbol(locale=LC_NUMERIC): + """Return the symbol used by the locale to separate decimal fractions. + + >>> get_decimal_symbol('en_US') + u'.' + + :param locale: the `Locale` object or locale identifier + :return: the decimal symbol + :rtype: `unicode` + """ + return Locale.parse(locale).number_symbols.get('decimal', u'.') + +def get_group_symbol(locale=LC_NUMERIC): + """Return the symbol used by the locale to separate groups of thousands. + + >>> get_group_symbol('en_US') + u',' + + :param locale: the `Locale` object or locale identifier + :return: the group symbol + :rtype: `unicode` + """ + return Locale.parse(locale).number_symbols.get('group', u'.') + +def format_number(number, locale=LC_NUMERIC): + """Returns the given number formatted for a specific locale. + + >>> format_number(1099, locale='en_US') + u'1,099' + + :param number: the number to format + :param locale: the `Locale` object or locale identifier + :return: the formatted number + :rtype: `unicode` + """ + group = get_group_symbol(locale) + if not group: + return unicode(number) + thou = re.compile(r'([0-9])([0-9][0-9][0-9]([%s]|$))' % group).search + v = str(number) + mo = thou(v) + while mo is not None: + l = mo.start(0) + v = v[:l+1] + group + v[l+1:] + mo = thou(v) + return unicode(v) + +def format_decimal(number, places=2, locale=LC_NUMERIC): + """Returns the given decimal number formatted for a specific locale. + + >>> format_decimal(1099.98, locale='en_US') + u'1,099.98' + + The appropriate thousands grouping and the decimal separator are used for + each locale: + + >>> format_decimal(1099.98, locale='de_DE') + u'1.099,98' + + The number of decimal places defaults to 2, but can also be specified + explicitly: + + >>> format_decimal(1099.98, places=4, locale='en_US') + u'1,099.9800' + + :param number: the number to format + :param places: the number of digit behind the decimal point + :param locale: the `Locale` object or locale identifier + :return: the formatted decimal number + :rtype: `unicode` + """ + locale = Locale.parse(locale) + a, b = (('%%.%df' % places) % number).split('.') + return unicode(format_number(a, locale) + get_decimal_symbol(locale) + b) + +def format_currency(value, locale=LC_NUMERIC): + """Returns formatted currency value. + + >>> format_currency(1099.98, locale='en_US') + u'1,099.98' + + :param value: the number to format + :param locale: the `Locale` object or locale identifier + :return: the formatted currency value + :rtype: `unicode` + """ + return format_decimal(value, places=2, locale=locale) + +def format_percent(value, places=2, locale=LC_NUMERIC): + raise NotImplementedError + +def format_scientific(value, locale=LC_NUMERIC): + raise NotImplementedError + +def parse_number(string, locale=LC_NUMERIC): + """Parse localized number string into a long integer. + + >>> parse_number('1,099', locale='en_US') + 1099L + >>> parse_number('1.099', locale='de_DE') + 1099L + + :param string: the string to parse + :param locale: the `Locale` object or locale identifier + :return: the parsed number + :rtype: `long` + :raise `ValueError`: if the string can not be converted to a number + """ + return long(string.replace(get_group_symbol(locale), '')) + +def parse_decimal(string, locale=LC_NUMERIC): + """Parse localized decimal string into a float. + + >>> parse_decimal('1,099.98', locale='en_US') + 1099.98 + >>> parse_decimal('1.099,98', locale='de_DE') + 1099.98 + + :param string: the string to parse + :param locale: the `Locale` object or locale identifier + :return: the parsed decimal number + :rtype: `float` + :raise `ValueError`: if the string can not be converted to a decimal number + """ + locale = Locale.parse(locale) + string = string.replace(get_group_symbol(locale), '') \ + .replace(get_decimal_symbol(locale), '.') + return float(string) diff --git a/babel/tests/__init__.py b/babel/tests/__init__.py new file mode 100644 index 0000000..fa6e2cc --- /dev/null +++ b/babel/tests/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import unittest + +def suite(): + from babel.tests import core, dates, numbers, util + from babel.catalog import tests as catalog + suite = unittest.TestSuite() + suite.addTest(core.suite()) + suite.addTest(dates.suite()) + suite.addTest(numbers.suite()) + suite.addTest(util.suite()) + suite.addTest(catalog.suite()) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/babel/tests/core.py b/babel/tests/core.py new file mode 100644 index 0000000..1337cf6 --- /dev/null +++ b/babel/tests/core.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import doctest +import unittest + +from babel import core + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(core)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/babel/tests/dates.py b/babel/tests/dates.py new file mode 100644 index 0000000..e614d85 --- /dev/null +++ b/babel/tests/dates.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import doctest +import unittest + +from babel import dates + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(dates)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/babel/tests/numbers.py b/babel/tests/numbers.py new file mode 100644 index 0000000..68a9dc4 --- /dev/null +++ b/babel/tests/numbers.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import doctest +import unittest + +from babel import numbers + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(numbers)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/babel/tests/util.py b/babel/tests/util.py new file mode 100644 index 0000000..0c780d3 --- /dev/null +++ b/babel/tests/util.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import doctest +import unittest + +from babel import util + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(util)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/babel/util.py b/babel/util.py new file mode 100644 index 0000000..b2b7725 --- /dev/null +++ b/babel/util.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +"""Various utility classes and functions.""" + +import os +import re + +__all__ = ['default_locale', 'extended_glob', 'lazy'] +__docformat__ = 'restructuredtext en' + +def default_locale(kind): + """Returns the default locale for a given category, based on environment + variables. + + :param kind: one of the ``LC_XXX`` environment variable names + :return: the value of the variable, or any of the fallbacks (``LC_ALL`` and + ``LANG``) + :rtype: `str` + """ + for name in (kind, 'LC_ALL', 'LANG'): + locale = os.getenv(name) + if locale is not None: + return locale + +def extended_glob(pattern, dirname=''): + """Extended pathname pattern expansion. + + This function is similar to what is provided by the ``glob`` module in the + Python standard library, but also supports a convenience pattern ("**") to + match files at any directory level. + + :param pattern: the glob pattern + :param dirname: the path to the directory in which to search for files + matching the given pattern + :return: an iterator over the absolute filenames of any matching files + :rtype: ``iterator`` + """ + symbols = { + '?': '[^/]', + '?/': '[^/]/', + '*': '[^/]+', + '*/': '[^/]+/', + '**': '(?:.+/)*?', + '**/': '(?:.+/)*?', + } + buf = [] + for idx, part in enumerate(re.split('([?*]+/?)', pattern)): + if idx % 2: + buf.append(symbols[part]) + elif part: + buf.append(re.escape(part)) + regex = re.compile(''.join(buf) + '$') + + absname = os.path.abspath(dirname) + for root, dirnames, filenames in os.walk(absname): + for subdir in dirnames: + if subdir.startswith('.') or subdir.startswith('_'): + dirnames.remove(subdir) + for filename in filenames: + filepath = relpath( + os.path.join(root, filename).replace(os.sep, '/'), + dirname + ) + if regex.match(filepath): + yield filepath + +def lazy(func): + """Return a new function that lazily evaluates another function. + + >>> lazystr = lazy(str) + >>> ls = lazystr('foo') + >>> print ls + foo + + :param func: the function to wrap + :return: a lazily-evaluated version of the function + :rtype: ``function`` + """ + def newfunc(*args, **kwargs): + return LazyProxy(func, *args, **kwargs) + return newfunc + + +class LazyProxy(object): + """ + + >>> lazystr = LazyProxy(str, 'bar') + >>> print lazystr + bar + >>> u'foo' + lazystr + u'foobar' + """ + + def __init__(self, func, *args, **kwargs): + self.func = func + self.args = args + self.kwargs = kwargs + self._value = None + + def value(self): + if self._value is None: + self._value = self.func(*self.args, **self.kwargs) + return self._value + value = property(value) + + def __str__(self): + return str(self.value) + + def __unicode__(self): + return unicode(self.value) + + def __add__(self, other): + return self.value + other + + def __radd__(self, other): + return other + self.value + + def __mod__(self, other): + return self.value % other + + def __rmod__(self, other): + return other % self.value + + def __mul__(self, other): + return self.value * other + + def __rmul__(self, other): + return other * self.value + + def __call__(self, *args, **kwargs): + return self.value(*args, **kwargs) + + def __cmp__(self, other): + return cmp(self.value, other) + + def __rcmp__(self, other): + return other + self.value + + def __eq__(self, other): + return self.value == other + +# def __delattr__(self, name): +# delattr(self.value, name) +# +# def __getattr__(self, name): +# return getattr(self.value, name) +# +# def __setattr__(self, name, value): +# setattr(self.value, name, value) + + def __delitem__(self, key): + del self.value[name] + + def __getitem__(self, key): + return self.value[name] + + def __setitem__(self, key, value): + self.value[name] = value + + +try: + relpath = os.path.relpath +except AttributeError: + def relpath(path, start='.'): + start_list = os.path.abspath(start).split(os.sep) + path_list = os.path.abspath(path).split(os.sep) + + # Work out how much of the filepath is shared by start and path. + i = len(os.path.commonprefix([start_list, path_list])) + + rel_list = [os.path.pardir] * (len(start_list) - i) + path_list[i:] + return os.path.join(*rel_list) diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py new file mode 100755 index 0000000..a13de62 --- /dev/null +++ b/scripts/import_cldr.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import copy +from optparse import OptionParser +import os +import pickle +import sys +try: + from xml.etree.ElementTree import parse +except ImportError: + from elementtree.ElementTree import parse + +from babel.dates import parse_pattern + +def _parent(locale): + parts = locale.split('_') + if len(parts) == 1: + return 'root' + else: + return '_'.join(parts[:-1]) + +def _text(elem): + buf = [elem.text or ''] + for child in elem: + buf.append(_text(child)) + buf.append(elem.tail or '') + return u''.join(filter(None, buf)).strip() + +def main(): + parser = OptionParser(usage='%prog path/to/cldr') + options, args = parser.parse_args() + if len(args) != 1: + parser.error('incorrect number of arguments') + + srcdir = args[0] + destdir = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), + '..', 'babel', 'localedata') + + filenames = os.listdir(os.path.join(srcdir, 'main')) + filenames.remove('root.xml') + filenames.sort(lambda a,b: len(a)-len(b)) + filenames.insert(0, 'root.xml') + + dicts = {} + + for filename in filenames: + print>>sys.stderr, 'Processing input file %r' % filename + stem, ext = os.path.splitext(filename) + if ext != '.xml': + continue + + data = {} + if stem != 'root': + data.update(copy.deepcopy(dicts[_parent(stem)])) + tree = parse(os.path.join(srcdir, 'main', filename)) + + # <localeDisplayNames> + + territories = data.setdefault('territories', {}) + for elem in tree.findall('//territories/territory'): + if 'draft' in elem.attrib and elem.attrib['type'] in territories: + continue + territories[elem.attrib['type']] = _text(elem) + + languages = data.setdefault('languages', {}) + for elem in tree.findall('//languages/language'): + if 'draft' in elem.attrib and elem.attrib['type'] in languages: + continue + languages[elem.attrib['type']] = _text(elem) + + variants = data.setdefault('variants', {}) + for elem in tree.findall('//variants/variant'): + if 'draft' in elem.attrib and elem.attrib['type'] in variants: + continue + variants[elem.attrib['type']] = _text(elem) + + scripts = data.setdefault('scripts', {}) + for elem in tree.findall('//scripts/script'): + if 'draft' in elem.attrib and elem.attrib['type'] in scripts: + continue + scripts[elem.attrib['type']] = _text(elem) + + # <dates> + + time_zones = data.setdefault('time_zones', {}) + for elem in tree.findall('//timeZoneNames/zone'): + time_zones[elem.tag] = unicode(elem.findtext('displayName')) + + for calendar in tree.findall('//calendars/calendar'): + if calendar.attrib['type'] != 'gregorian': + # TODO: support other calendar types + continue + + months = data.setdefault('months', {}) + for ctxt in calendar.findall('months/monthContext'): + ctxts = months.setdefault(ctxt.attrib['type'], {}) + for width in ctxt.findall('monthWidth'): + widths = ctxts.setdefault(width.attrib['type'], {}) + for elem in width.findall('month'): + if 'draft' in elem.attrib and int(elem.attrib['type']) in widths: + continue + widths[int(elem.attrib.get('type'))] = unicode(elem.text) + + days = data.setdefault('days', {}) + for ctxt in calendar.findall('days/dayContext'): + ctxts = days.setdefault(ctxt.attrib['type'], {}) + for width in ctxt.findall('dayWidth'): + widths = ctxts.setdefault(width.attrib['type'], {}) + for elem in width.findall('day'): + dtype = {'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4, + 'fri': 5, 'sat': 6, 'sun': 7}[elem.attrib['type']] + if 'draft' in elem.attrib and dtype in widths: + continue + widths[dtype] = unicode(elem.text) + + quarters = data.setdefault('quarters', {}) + for ctxt in calendar.findall('quarters/quarterContext'): + ctxts = quarters.setdefault(ctxt.attrib['type'], {}) + for width in ctxt.findall('quarterWidth'): + widths = ctxts.setdefault(width.attrib['type'], {}) + for elem in width.findall('quarter'): + if 'draft' in elem.attrib and int(elem.attrib['type']) in widths: + continue + widths[int(elem.attrib.get('type'))] = unicode(elem.text) + + eras = data.setdefault('eras', {}) + for width in calendar.findall('eras/*'): + ewidth = {'eraNames': 'wide', 'eraAbbr': 'abbreviated'}[width.tag] + widths = eras.setdefault(ewidth, {}) + for elem in width.findall('era'): + if 'draft' in elem.attrib and int(elem.attrib['type']) in widths: + continue + widths[int(elem.attrib.get('type'))] = unicode(elem.text) + + # AM/PM + periods = data.setdefault('periods', {}) + for elem in calendar.findall('am'): + if 'draft' in elem.attrib and elem.tag in periods: + continue + periods[elem.tag] = unicode(elem.text) + for elem in calendar.findall('pm'): + if 'draft' in elem.attrib and elem.tag in periods: + continue + periods[elem.tag] = unicode(elem.text) + + date_formats = data.setdefault('date_formats', {}) + for elem in calendar.findall('dateFormats/dateFormatLength'): + if 'draft' in elem.attrib and elem.attrib.get('type') in date_formats: + continue + try: + date_formats[elem.attrib.get('type')] = \ + parse_pattern(unicode(elem.findtext('dateFormat/pattern'))) + except ValueError, e: + print e + + time_formats = data.setdefault('time_formats', {}) + for elem in calendar.findall('timeFormats/timeFormatLength'): + if 'draft' in elem.attrib and elem.attrib.get('type') in time_formats: + continue + try: + time_formats[elem.attrib.get('type')] = \ + parse_pattern(unicode(elem.findtext('timeFormat/pattern'))) + except ValueError, e: + print e + + # <numbers> + + number_symbols = data.setdefault('number_symbols', {}) + for elem in tree.findall('//numbers/symbols/*'): + number_symbols[elem.tag] = unicode(elem.text) + + decimal_formats = data.setdefault('decimal_formats', {}) + for elem in tree.findall('//decimalFormats/decimalFormatLength'): + if 'draft' in elem.attrib and elem.attrib.get('type') in decimal_formats: + continue + decimal_formats[elem.attrib.get('type')] = unicode(elem.findtext('decimalFormat/pattern')) + + scientific_formats = data.setdefault('scientific_formats', {}) + for elem in tree.findall('//scientificFormats/scientificFormatLength'): + if 'draft' in elem.attrib and elem.attrib.get('type') in scientific_formats: + continue + scientific_formats[elem.attrib.get('type')] = unicode(elem.findtext('scientificFormat/pattern')) + + currency_formats = data.setdefault('currency_formats', {}) + for elem in tree.findall('//currencyFormats/currencyFormatLength'): + if 'draft' in elem.attrib and elem.attrib.get('type') in currency_formats: + continue + currency_formats[elem.attrib.get('type')] = unicode(elem.findtext('currencyFormat/pattern')) + + percent_formats = data.setdefault('percent_formats', {}) + for elem in tree.findall('//percentFormats/percentFormatLength'): + if 'draft' in elem.attrib and elem.attrib.get('type') in percent_formats: + continue + percent_formats[elem.attrib.get('type')] = unicode(elem.findtext('percentFormat/pattern')) + + currencies = data.setdefault('currencies', {}) + for elem in tree.findall('//currencies/currency'): + currencies[elem.attrib['type']] = { + 'display_name': unicode(elem.findtext('displayName')), + 'symbol': unicode(elem.findtext('symbol')) + } + + dicts[stem] = data + outfile = open(os.path.join(destdir, stem + '.dat'), 'wb') + try: + pickle.dump(data, outfile, 2) + finally: + outfile.close() + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..8ad82af --- /dev/null +++ b/setup.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import doctest +from glob import glob +import os +from setuptools import find_packages, setup, Command +import sys + + +class build_doc(Command): + description = 'Builds the documentation' + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + from docutils.core import publish_cmdline + docutils_conf = os.path.join('doc', 'docutils.conf') + epydoc_conf = os.path.join('doc', 'epydoc.conf') + + for source in glob('doc/*.txt'): + dest = os.path.splitext(source)[0] + '.html' + if not os.path.exists(dest) or \ + os.path.getmtime(dest) < os.path.getmtime(source): + print 'building documentation file %s' % dest + publish_cmdline(writer_name='html', + argv=['--config=%s' % docutils_conf, source, + dest]) + + try: + from epydoc import cli + old_argv = sys.argv[1:] + sys.argv[1:] = [ + '--config=%s' % epydoc_conf, + '--no-private', # epydoc bug, not read from config + '--simple-term', + '--verbose' + ] + cli.cli() + sys.argv[1:] = old_argv + + except ImportError: + print 'epydoc not installed, skipping API documentation.' + + +class test_doc(Command): + description = 'Tests the code examples in the documentation' + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + for filename in glob('doc/*.txt'): + print 'testing documentation file %s' % filename + doctest.testfile(filename, False, optionflags=doctest.ELLIPSIS) + + +setup( + name = 'Babel', + version = '0.1', + description = 'Internationalization utilities', + long_description = \ +"""A collection of tools for internationalizing Python applications.""", + author = 'Edgewall Software', + author_email = 'info@edgewall.org', + license = 'BSD', + url = 'http://babel.edgewall.org/', + download_url = 'http://babel.edgewall.org/wiki/Download', + zip_safe = False, + + classifiers = [ + 'Development Status :: 4 - Beta', + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + packages = find_packages(exclude=['tests']), + package_data = {'babel': ['localedata/*.dat']}, + test_suite = 'babel.tests.suite', + + entry_points = """ + [console_scripts] + pygettext = babel.catalog.frontend:main + + [distutils.commands] + extract_messages = babel.catalog.frontend:extract_messages + + [babel.extractors] + genshi = babel.catalog.extract:extract_genshi + python = babel.catalog.extract:extract_python + """, + + cmdclass = {'build_doc': build_doc, 'test_doc': test_doc} +) |