diff --git a/index.yaml b/index.yaml index 0374c75..d715dc5 100644 --- a/index.yaml +++ b/index.yaml @@ -10,6 +10,12 @@ indexes: # automatically uploaded to the admin console when you next deploy # your application using appcfg.py. +- kind: Map + properties: + - name: name + - name: date_created + direction: desc + - kind: Simulation properties: - name: name diff --git a/justice/external/babel/__init__.py b/justice/external/babel/__init__.py index b0b79c6..5c54827 100644 --- a/justice/external/babel/__init__.py +++ b/justice/external/babel/__init__.py @@ -1,39 +1,39 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007-2008 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 * - -__docformat__ = 'restructuredtext en' -try: - from pkg_resources import get_distribution, ResolutionError - try: - __version__ = get_distribution('Babel').version - except ResolutionError: - __version__ = None # unknown -except ImportError: - __version__ = None # unknown +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007-2008 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 * + +__docformat__ = 'restructuredtext en' +try: + from pkg_resources import get_distribution, ResolutionError + try: + __version__ = get_distribution('Babel').version + except ResolutionError: + __version__ = None # unknown +except ImportError: + __version__ = None # unknown diff --git a/justice/external/babel/core.py b/justice/external/babel/core.py index a5f108b..e6ef5db 100644 --- a/justice/external/babel/core.py +++ b/justice/external/babel/core.py @@ -1,790 +1,790 @@ -# -*- 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/. - -"""Core locale representation and locale data access.""" - -import os -import pickle - -from babel import localedata - -__all__ = ['UnknownLocaleError', 'Locale', 'default_locale', 'negotiate_locale', - 'parse_locale'] -__docformat__ = 'restructuredtext en' - -_global_data = None - -def get_global(key): - """Return the dictionary for the given key in the global data. - - The global data is stored in the ``babel/global.dat`` file and contains - information independent of individual locales. - - >>> get_global('zone_aliases')['UTC'] - 'Etc/GMT' - >>> get_global('zone_territories')['Europe/Berlin'] - 'DE' - - :param key: the data key - :return: the dictionary found in the global data under the given key - :rtype: `dict` - :since: version 0.9 - """ - global _global_data - if _global_data is None: - dirname = os.path.join(os.path.dirname(__file__)) - filename = os.path.join(dirname, 'global.dat') - fileobj = open(filename, 'rb') - try: - _global_data = pickle.load(fileobj) - finally: - fileobj.close() - return _global_data.get(key, {}) - - -LOCALE_ALIASES = { - 'ar': 'ar_SY', 'bg': 'bg_BG', 'bs': 'bs_BA', 'ca': 'ca_ES', 'cs': 'cs_CZ', - 'da': 'da_DK', 'de': 'de_DE', 'el': 'el_GR', 'en': 'en_US', 'es': 'es_ES', - 'et': 'et_EE', 'fa': 'fa_IR', 'fi': 'fi_FI', 'fr': 'fr_FR', 'gl': 'gl_ES', - 'he': 'he_IL', 'hu': 'hu_HU', 'id': 'id_ID', 'is': 'is_IS', 'it': 'it_IT', - 'ja': 'ja_JP', 'km': 'km_KH', 'ko': 'ko_KR', 'lt': 'lt_LT', 'lv': 'lv_LV', - 'mk': 'mk_MK', 'nl': 'nl_NL', 'nn': 'nn_NO', 'no': 'nb_NO', 'pl': 'pl_PL', - 'pt': 'pt_PT', 'ro': 'ro_RO', 'ru': 'ru_RU', 'sk': 'sk_SK', 'sl': 'sl_SI', - 'sv': 'sv_SE', 'th': 'th_TH', 'tr': 'tr_TR', 'uk': 'uk_UA' -} - - -class UnknownLocaleError(Exception): - """Exception thrown when a locale is requested for which no locale data - is available. - """ - - def __init__(self, identifier): - """Create the exception. - - :param identifier: the identifier string of the unsupported locale - """ - Exception.__init__(self, 'unknown locale %r' % identifier) - self.identifier = identifier - - -class Locale(object): - """Representation of a specific locale. - - >>> locale = Locale('en', 'US') - >>> repr(locale) - '' - >>> 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` 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'.' - - If a locale is requested for which no locale data is available, an - `UnknownLocaleError` is raised: - - >>> Locale.parse('en_DE') - Traceback (most recent call last): - ... - UnknownLocaleError: unknown locale 'en_DE' - - :see: `IETF RFC 3066 `_ - """ - - def __init__(self, language, territory=None, script=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 script: the script code - :param variant: the variant code - :raise `UnknownLocaleError`: if no locale data is available for the - requested locale - """ - self.language = language - self.territory = territory - self.script = script - self.variant = variant - self.__data = None - - identifier = str(self) - if not localedata.exists(identifier): - raise UnknownLocaleError(identifier) - - def default(cls, category=None, aliases=LOCALE_ALIASES): - """Return the system default locale for the specified category. - - >>> for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE']: - ... os.environ[name] = '' - >>> os.environ['LANG'] = 'fr_FR.UTF-8' - >>> Locale.default('LC_MESSAGES') - - - :param category: one of the ``LC_XXX`` environment variable names - :param aliases: a dictionary of aliases for locale identifiers - :return: the value of the variable, or any of the fallbacks - (``LANGUAGE``, ``LC_ALL``, ``LC_CTYPE``, and ``LANG``) - :rtype: `Locale` - :see: `default_locale` - """ - return cls(default_locale(category, aliases=aliases)) - default = classmethod(default) - - def negotiate(cls, preferred, available, sep='_', aliases=LOCALE_ALIASES): - """Find the best match between available and requested locale strings. - - >>> Locale.negotiate(['de_DE', 'en_US'], ['de_DE', 'de_AT']) - - >>> Locale.negotiate(['de_DE', 'en_US'], ['en', 'de']) - - >>> Locale.negotiate(['de_DE', 'de'], ['en_US']) - - You can specify the character used in the locale identifiers to separate - the differnet components. This separator is applied to both lists. Also, - case is ignored in the comparison: - - >>> Locale.negotiate(['de-DE', 'de'], ['en-us', 'de-de'], sep='-') - - - :param preferred: the list of locale identifers preferred by the user - :param available: the list of locale identifiers available - :param aliases: a dictionary of aliases for locale identifiers - :return: the `Locale` object for the best match, or `None` if no match - was found - :rtype: `Locale` - :see: `negotiate_locale` - """ - identifier = negotiate_locale(preferred, available, sep=sep, - aliases=aliases) - if identifier: - return Locale.parse(identifier, sep=sep) - negotiate = classmethod(negotiate) - - 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) - - - :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 - :raise `UnknownLocaleError`: if no locale data is available for the - requested locale - :see: `parse_locale` - """ - if isinstance(identifier, basestring): - return cls(*parse_locale(identifier, sep=sep)) - return identifier - parse = classmethod(parse) - - def __eq__(self, other): - return str(self) == str(other) - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return '' % str(self) - - def __str__(self): - return '_'.join(filter(None, [self.language, self.script, - self.territory, self.variant])) - - def _data(self): - if self.__data is None: - self.__data = localedata.LocaleDataDict(localedata.load(str(self))) - return self.__data - _data = property(_data) - - def get_display_name(self, locale=None): - """Return the display name of the locale using the given locale. - - The display name will include the language, territory, script, and - variant, if those are specified. - - >>> Locale('zh', 'CN', script='Hans').get_display_name('en') - u'Chinese (Simplified Han, China)' - - :param locale: the locale to use - :return: the display name - """ - if locale is None: - locale = self - locale = Locale.parse(locale) - retval = locale.languages.get(self.language) - if self.territory or self.script or self.variant: - details = [] - if self.script: - details.append(locale.scripts.get(self.script)) - if self.territory: - details.append(locale.territories.get(self.territory)) - if self.variant: - details.append(locale.variants.get(self.variant)) - details = filter(None, details) - if details: - retval += ' (%s)' % u', '.join(details) - return retval - - display_name = property(get_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)' - >>> Locale('sv').display_name - u'svenska' - - :type: `unicode` - """) - - def english_name(self): - return self.get_display_name(Locale('en')) - english_name = property(english_name, doc="""\ - The english display name of the locale. - - >>> Locale('de').english_name - u'German' - >>> Locale('de', 'DE').english_name - u'German (Germany)' - - :type: `unicode` - """) - - #{ General Locale Display Names - - 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 `_ - """) - - 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 `_ - """) - - 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 `_ - """) - - 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` - """) - - #{ Number Formatting - - def currencies(self): - return self._data['currency_names'] - currencies = property(currencies, doc="""\ - Mapping of currency codes to translated currency names. - - >>> Locale('en').currencies['COP'] - u'Colombian Peso' - >>> Locale('de', 'DE').currencies['COP'] - u'Kolumbianischer Peso' - - :type: `dict` - """) - - def currency_symbols(self): - return self._data['currency_symbols'] - currency_symbols = property(currency_symbols, doc="""\ - Mapping of currency codes to symbols. - - >>> Locale('en', 'US').currency_symbols['USD'] - u'$' - >>> Locale('es', 'CO').currency_symbols['USD'] - u'US$' - - :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 decimal_formats(self): - return self._data['decimal_formats'] - decimal_formats = property(decimal_formats, doc="""\ - Locale patterns for decimal number formatting. - - >>> Locale('en', 'US').decimal_formats[None] - - - :type: `dict` - """) - - def currency_formats(self): - return self._data['currency_formats'] - currency_formats = property(currency_formats, doc=r"""\ - Locale patterns for currency number formatting. - - >>> print Locale('en', 'US').currency_formats[None] - - - :type: `dict` - """) - - def percent_formats(self): - return self._data['percent_formats'] - percent_formats = property(percent_formats, doc="""\ - Locale patterns for percent number formatting. - - >>> Locale('en', 'US').percent_formats[None] - - - :type: `dict` - """) - - def scientific_formats(self): - return self._data['scientific_formats'] - scientific_formats = property(scientific_formats, doc="""\ - Locale patterns for scientific number formatting. - - >>> Locale('en', 'US').scientific_formats[None] - - - :type: `dict` - """) - - #{ Calendar Information and Date Formatting - - 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'][3] - 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 time_zones(self): - return self._data['time_zones'] - time_zones = property(time_zones, doc="""\ - Locale display names for time zones. - - >>> Locale('en', 'US').time_zones['Europe/London']['long']['daylight'] - u'British Summer Time' - >>> Locale('en', 'US').time_zones['America/St_Johns']['city'] - u"St. John's" - - :type: `dict` - """) - - def meta_zones(self): - return self._data['meta_zones'] - meta_zones = property(meta_zones, doc="""\ - Locale display names for meta time zones. - - Meta time zones are basically groups of different Olson time zones that - have the same GMT offset and daylight savings time. - - >>> Locale('en', 'US').meta_zones['Europe_Central']['long']['daylight'] - u'Central European Summer Time' - - :type: `dict` - :since: version 0.9 - """) - - def zone_formats(self): - return self._data['zone_formats'] - zone_formats = property(zone_formats, doc=r"""\ - Patterns related to the formatting of time zones. - - >>> Locale('en', 'US').zone_formats['fallback'] - u'%(1)s (%(0)s)' - >>> Locale('pt', 'BR').zone_formats['region'] - u'Hor\xe1rio %s' - - :type: `dict` - :since: version 0.9 - """) - - def first_week_day(self): - return self._data['week_data']['first_day'] - first_week_day = property(first_week_day, doc="""\ - The first day of a week, with 0 being Monday. - - >>> Locale('de', 'DE').first_week_day - 0 - >>> Locale('en', 'US').first_week_day - 6 - - :type: `int` - """) - - def weekend_start(self): - return self._data['week_data']['weekend_start'] - weekend_start = property(weekend_start, doc="""\ - The day the weekend starts, with 0 being Monday. - - >>> Locale('de', 'DE').weekend_start - 5 - - :type: `int` - """) - - def weekend_end(self): - return self._data['week_data']['weekend_end'] - weekend_end = property(weekend_end, doc="""\ - The day the weekend ends, with 0 being Monday. - - >>> Locale('de', 'DE').weekend_end - 6 - - :type: `int` - """) - - def min_week_days(self): - return self._data['week_data']['min_days'] - min_week_days = property(min_week_days, doc="""\ - The minimum number of days in a week so that the week is counted as the - first week of a year or month. - - >>> Locale('de', 'DE').min_week_days - 4 - - :type: `int` - """) - - 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'] - - >>> Locale('fr', 'FR').date_formats['long'] - - - :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'] - - >>> Locale('fr', 'FR').time_formats['long'] - - - :type: `dict` - """) - - def datetime_formats(self): - return self._data['datetime_formats'] - datetime_formats = property(datetime_formats, doc="""\ - Locale patterns for datetime formatting. - - >>> Locale('en').datetime_formats[None] - u'{1} {0}' - >>> Locale('th').datetime_formats[None] - u'{1}, {0}' - - :type: `dict` - """) - - -def default_locale(category=None, aliases=LOCALE_ALIASES): - """Returns the system default locale for a given category, based on - environment variables. - - >>> for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE']: - ... os.environ[name] = '' - >>> os.environ['LANG'] = 'fr_FR.UTF-8' - >>> default_locale('LC_MESSAGES') - 'fr_FR' - - The "C" or "POSIX" pseudo-locales are treated as aliases for the - "en_US_POSIX" locale: - - >>> os.environ['LC_MESSAGES'] = 'POSIX' - >>> default_locale('LC_MESSAGES') - 'en_US_POSIX' - - :param category: one of the ``LC_XXX`` environment variable names - :param aliases: a dictionary of aliases for locale identifiers - :return: the value of the variable, or any of the fallbacks (``LANGUAGE``, - ``LC_ALL``, ``LC_CTYPE``, and ``LANG``) - :rtype: `str` - """ - varnames = (category, 'LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG') - for name in filter(None, varnames): - locale = os.getenv(name) - if locale: - if name == 'LANGUAGE' and ':' in locale: - # the LANGUAGE variable may contain a colon-separated list of - # language codes; we just pick the language on the list - locale = locale.split(':')[0] - if locale in ('C', 'POSIX'): - locale = 'en_US_POSIX' - elif aliases and locale in aliases: - locale = aliases[locale] - try: - return '_'.join(filter(None, parse_locale(locale))) - except ValueError: - pass - -def negotiate_locale(preferred, available, sep='_', aliases=LOCALE_ALIASES): - """Find the best match between available and requested locale strings. - - >>> negotiate_locale(['de_DE', 'en_US'], ['de_DE', 'de_AT']) - 'de_DE' - >>> negotiate_locale(['de_DE', 'en_US'], ['en', 'de']) - 'de' - - Case is ignored by the algorithm, the result uses the case of the preferred - locale identifier: - - >>> negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at']) - 'de_DE' - - >>> negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at']) - 'de_DE' - - By default, some web browsers unfortunately do not include the territory - in the locale identifier for many locales, and some don't even allow the - user to easily add the territory. So while you may prefer using qualified - locale identifiers in your web-application, they would not normally match - the language-only locale sent by such browsers. To workaround that, this - function uses a default mapping of commonly used langauge-only locale - identifiers to identifiers including the territory: - - >>> negotiate_locale(['ja', 'en_US'], ['ja_JP', 'en_US']) - 'ja_JP' - - Some browsers even use an incorrect or outdated language code, such as "no" - for Norwegian, where the correct locale identifier would actually be "nb_NO" - (Bokmål) or "nn_NO" (Nynorsk). The aliases are intended to take care of - such cases, too: - - >>> negotiate_locale(['no', 'sv'], ['nb_NO', 'sv_SE']) - 'nb_NO' - - You can override this default mapping by passing a different `aliases` - dictionary to this function, or you can bypass the behavior althogher by - setting the `aliases` parameter to `None`. - - :param preferred: the list of locale strings preferred by the user - :param available: the list of locale strings available - :param sep: character that separates the different parts of the locale - strings - :param aliases: a dictionary of aliases for locale identifiers - :return: the locale identifier for the best match, or `None` if no match - was found - :rtype: `str` - """ - available = [a.lower() for a in available if a] - for locale in preferred: - ll = locale.lower() - if ll in available: - return locale - if aliases: - alias = aliases.get(ll) - if alias: - alias = alias.replace('_', sep) - if alias.lower() in available: - return alias - parts = locale.split(sep) - if len(parts) > 1 and parts[0].lower() in available: - return parts[0] - return None - -def parse_locale(identifier, sep='_'): - """Parse a locale identifier into a tuple of the form:: - - ``(language, territory, script, variant)`` - - >>> parse_locale('zh_CN') - ('zh', 'CN', None, None) - >>> parse_locale('zh_Hans_CN') - ('zh', 'CN', 'Hans', None) - - The default component separator is "_", but a different separator can be - specified using the `sep` parameter: - - >>> parse_locale('zh-CN', sep='-') - ('zh', 'CN', None, None) - - If the identifier cannot be parsed into a locale, a `ValueError` exception - is raised: - - >>> parse_locale('not_a_LOCALE_String') - Traceback (most recent call last): - ... - ValueError: 'not_a_LOCALE_String' is not a valid locale identifier - - Encoding information and locale modifiers are removed from the identifier: - - >>> parse_locale('it_IT@euro') - ('it', 'IT', None, None) - >>> parse_locale('en_US.UTF-8') - ('en', 'US', None, None) - >>> parse_locale('de_DE.iso885915@euro') - ('de', 'DE', None, None) - - :param identifier: the locale identifier string - :param sep: character that separates the different components of the locale - identifier - :return: the ``(language, territory, script, variant)`` tuple - :rtype: `tuple` - :raise `ValueError`: if the string does not appear to be a valid locale - identifier - - :see: `IETF RFC 4646 `_ - """ - if '.' in identifier: - # this is probably the charset/encoding, which we don't care about - identifier = identifier.split('.', 1)[0] - if '@' in identifier: - # this is a locale modifier such as @euro, which we don't care about - # either - identifier = identifier.split('@', 1)[0] - - parts = identifier.split(sep) - lang = parts.pop(0).lower() - if not lang.isalpha(): - raise ValueError('expected only letters, got %r' % lang) - - script = territory = variant = None - if parts: - if len(parts[0]) == 4 and parts[0].isalpha(): - script = parts.pop(0).title() - - if parts: - if len(parts[0]) == 2 and parts[0].isalpha(): - territory = parts.pop(0).upper() - elif len(parts[0]) == 3 and parts[0].isdigit(): - territory = parts.pop(0) - - if parts: - if len(parts[0]) == 4 and parts[0][0].isdigit() or \ - len(parts[0]) >= 5 and parts[0][0].isalpha(): - variant = parts.pop() - - if parts: - raise ValueError('%r is not a valid locale identifier' % identifier) - - return lang, territory, script, variant +# -*- 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/. + +"""Core locale representation and locale data access.""" + +import os +import pickle + +from babel import localedata + +__all__ = ['UnknownLocaleError', 'Locale', 'default_locale', 'negotiate_locale', + 'parse_locale'] +__docformat__ = 'restructuredtext en' + +_global_data = None + +def get_global(key): + """Return the dictionary for the given key in the global data. + + The global data is stored in the ``babel/global.dat`` file and contains + information independent of individual locales. + + >>> get_global('zone_aliases')['UTC'] + 'Etc/GMT' + >>> get_global('zone_territories')['Europe/Berlin'] + 'DE' + + :param key: the data key + :return: the dictionary found in the global data under the given key + :rtype: `dict` + :since: version 0.9 + """ + global _global_data + if _global_data is None: + dirname = os.path.join(os.path.dirname(__file__)) + filename = os.path.join(dirname, 'global.dat') + fileobj = open(filename, 'rb') + try: + _global_data = pickle.load(fileobj) + finally: + fileobj.close() + return _global_data.get(key, {}) + + +LOCALE_ALIASES = { + 'ar': 'ar_SY', 'bg': 'bg_BG', 'bs': 'bs_BA', 'ca': 'ca_ES', 'cs': 'cs_CZ', + 'da': 'da_DK', 'de': 'de_DE', 'el': 'el_GR', 'en': 'en_US', 'es': 'es_ES', + 'et': 'et_EE', 'fa': 'fa_IR', 'fi': 'fi_FI', 'fr': 'fr_FR', 'gl': 'gl_ES', + 'he': 'he_IL', 'hu': 'hu_HU', 'id': 'id_ID', 'is': 'is_IS', 'it': 'it_IT', + 'ja': 'ja_JP', 'km': 'km_KH', 'ko': 'ko_KR', 'lt': 'lt_LT', 'lv': 'lv_LV', + 'mk': 'mk_MK', 'nl': 'nl_NL', 'nn': 'nn_NO', 'no': 'nb_NO', 'pl': 'pl_PL', + 'pt': 'pt_PT', 'ro': 'ro_RO', 'ru': 'ru_RU', 'sk': 'sk_SK', 'sl': 'sl_SI', + 'sv': 'sv_SE', 'th': 'th_TH', 'tr': 'tr_TR', 'uk': 'uk_UA' +} + + +class UnknownLocaleError(Exception): + """Exception thrown when a locale is requested for which no locale data + is available. + """ + + def __init__(self, identifier): + """Create the exception. + + :param identifier: the identifier string of the unsupported locale + """ + Exception.__init__(self, 'unknown locale %r' % identifier) + self.identifier = identifier + + +class Locale(object): + """Representation of a specific locale. + + >>> locale = Locale('en', 'US') + >>> repr(locale) + '' + >>> 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` 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'.' + + If a locale is requested for which no locale data is available, an + `UnknownLocaleError` is raised: + + >>> Locale.parse('en_DE') + Traceback (most recent call last): + ... + UnknownLocaleError: unknown locale 'en_DE' + + :see: `IETF RFC 3066 `_ + """ + + def __init__(self, language, territory=None, script=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 script: the script code + :param variant: the variant code + :raise `UnknownLocaleError`: if no locale data is available for the + requested locale + """ + self.language = language + self.territory = territory + self.script = script + self.variant = variant + self.__data = None + + identifier = str(self) + if not localedata.exists(identifier): + raise UnknownLocaleError(identifier) + + def default(cls, category=None, aliases=LOCALE_ALIASES): + """Return the system default locale for the specified category. + + >>> for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE']: + ... os.environ[name] = '' + >>> os.environ['LANG'] = 'fr_FR.UTF-8' + >>> Locale.default('LC_MESSAGES') + + + :param category: one of the ``LC_XXX`` environment variable names + :param aliases: a dictionary of aliases for locale identifiers + :return: the value of the variable, or any of the fallbacks + (``LANGUAGE``, ``LC_ALL``, ``LC_CTYPE``, and ``LANG``) + :rtype: `Locale` + :see: `default_locale` + """ + return cls(default_locale(category, aliases=aliases)) + default = classmethod(default) + + def negotiate(cls, preferred, available, sep='_', aliases=LOCALE_ALIASES): + """Find the best match between available and requested locale strings. + + >>> Locale.negotiate(['de_DE', 'en_US'], ['de_DE', 'de_AT']) + + >>> Locale.negotiate(['de_DE', 'en_US'], ['en', 'de']) + + >>> Locale.negotiate(['de_DE', 'de'], ['en_US']) + + You can specify the character used in the locale identifiers to separate + the differnet components. This separator is applied to both lists. Also, + case is ignored in the comparison: + + >>> Locale.negotiate(['de-DE', 'de'], ['en-us', 'de-de'], sep='-') + + + :param preferred: the list of locale identifers preferred by the user + :param available: the list of locale identifiers available + :param aliases: a dictionary of aliases for locale identifiers + :return: the `Locale` object for the best match, or `None` if no match + was found + :rtype: `Locale` + :see: `negotiate_locale` + """ + identifier = negotiate_locale(preferred, available, sep=sep, + aliases=aliases) + if identifier: + return Locale.parse(identifier, sep=sep) + negotiate = classmethod(negotiate) + + 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) + + + :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 + :raise `UnknownLocaleError`: if no locale data is available for the + requested locale + :see: `parse_locale` + """ + if isinstance(identifier, basestring): + return cls(*parse_locale(identifier, sep=sep)) + return identifier + parse = classmethod(parse) + + def __eq__(self, other): + return str(self) == str(other) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return '' % str(self) + + def __str__(self): + return '_'.join(filter(None, [self.language, self.script, + self.territory, self.variant])) + + def _data(self): + if self.__data is None: + self.__data = localedata.LocaleDataDict(localedata.load(str(self))) + return self.__data + _data = property(_data) + + def get_display_name(self, locale=None): + """Return the display name of the locale using the given locale. + + The display name will include the language, territory, script, and + variant, if those are specified. + + >>> Locale('zh', 'CN', script='Hans').get_display_name('en') + u'Chinese (Simplified Han, China)' + + :param locale: the locale to use + :return: the display name + """ + if locale is None: + locale = self + locale = Locale.parse(locale) + retval = locale.languages.get(self.language) + if self.territory or self.script or self.variant: + details = [] + if self.script: + details.append(locale.scripts.get(self.script)) + if self.territory: + details.append(locale.territories.get(self.territory)) + if self.variant: + details.append(locale.variants.get(self.variant)) + details = filter(None, details) + if details: + retval += ' (%s)' % u', '.join(details) + return retval + + display_name = property(get_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)' + >>> Locale('sv').display_name + u'svenska' + + :type: `unicode` + """) + + def english_name(self): + return self.get_display_name(Locale('en')) + english_name = property(english_name, doc="""\ + The english display name of the locale. + + >>> Locale('de').english_name + u'German' + >>> Locale('de', 'DE').english_name + u'German (Germany)' + + :type: `unicode` + """) + + #{ General Locale Display Names + + 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 `_ + """) + + 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 `_ + """) + + 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 `_ + """) + + 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` + """) + + #{ Number Formatting + + def currencies(self): + return self._data['currency_names'] + currencies = property(currencies, doc="""\ + Mapping of currency codes to translated currency names. + + >>> Locale('en').currencies['COP'] + u'Colombian Peso' + >>> Locale('de', 'DE').currencies['COP'] + u'Kolumbianischer Peso' + + :type: `dict` + """) + + def currency_symbols(self): + return self._data['currency_symbols'] + currency_symbols = property(currency_symbols, doc="""\ + Mapping of currency codes to symbols. + + >>> Locale('en', 'US').currency_symbols['USD'] + u'$' + >>> Locale('es', 'CO').currency_symbols['USD'] + u'US$' + + :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 decimal_formats(self): + return self._data['decimal_formats'] + decimal_formats = property(decimal_formats, doc="""\ + Locale patterns for decimal number formatting. + + >>> Locale('en', 'US').decimal_formats[None] + + + :type: `dict` + """) + + def currency_formats(self): + return self._data['currency_formats'] + currency_formats = property(currency_formats, doc=r"""\ + Locale patterns for currency number formatting. + + >>> print Locale('en', 'US').currency_formats[None] + + + :type: `dict` + """) + + def percent_formats(self): + return self._data['percent_formats'] + percent_formats = property(percent_formats, doc="""\ + Locale patterns for percent number formatting. + + >>> Locale('en', 'US').percent_formats[None] + + + :type: `dict` + """) + + def scientific_formats(self): + return self._data['scientific_formats'] + scientific_formats = property(scientific_formats, doc="""\ + Locale patterns for scientific number formatting. + + >>> Locale('en', 'US').scientific_formats[None] + + + :type: `dict` + """) + + #{ Calendar Information and Date Formatting + + 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'][3] + 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 time_zones(self): + return self._data['time_zones'] + time_zones = property(time_zones, doc="""\ + Locale display names for time zones. + + >>> Locale('en', 'US').time_zones['Europe/London']['long']['daylight'] + u'British Summer Time' + >>> Locale('en', 'US').time_zones['America/St_Johns']['city'] + u"St. John's" + + :type: `dict` + """) + + def meta_zones(self): + return self._data['meta_zones'] + meta_zones = property(meta_zones, doc="""\ + Locale display names for meta time zones. + + Meta time zones are basically groups of different Olson time zones that + have the same GMT offset and daylight savings time. + + >>> Locale('en', 'US').meta_zones['Europe_Central']['long']['daylight'] + u'Central European Summer Time' + + :type: `dict` + :since: version 0.9 + """) + + def zone_formats(self): + return self._data['zone_formats'] + zone_formats = property(zone_formats, doc=r"""\ + Patterns related to the formatting of time zones. + + >>> Locale('en', 'US').zone_formats['fallback'] + u'%(1)s (%(0)s)' + >>> Locale('pt', 'BR').zone_formats['region'] + u'Hor\xe1rio %s' + + :type: `dict` + :since: version 0.9 + """) + + def first_week_day(self): + return self._data['week_data']['first_day'] + first_week_day = property(first_week_day, doc="""\ + The first day of a week, with 0 being Monday. + + >>> Locale('de', 'DE').first_week_day + 0 + >>> Locale('en', 'US').first_week_day + 6 + + :type: `int` + """) + + def weekend_start(self): + return self._data['week_data']['weekend_start'] + weekend_start = property(weekend_start, doc="""\ + The day the weekend starts, with 0 being Monday. + + >>> Locale('de', 'DE').weekend_start + 5 + + :type: `int` + """) + + def weekend_end(self): + return self._data['week_data']['weekend_end'] + weekend_end = property(weekend_end, doc="""\ + The day the weekend ends, with 0 being Monday. + + >>> Locale('de', 'DE').weekend_end + 6 + + :type: `int` + """) + + def min_week_days(self): + return self._data['week_data']['min_days'] + min_week_days = property(min_week_days, doc="""\ + The minimum number of days in a week so that the week is counted as the + first week of a year or month. + + >>> Locale('de', 'DE').min_week_days + 4 + + :type: `int` + """) + + 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'] + + >>> Locale('fr', 'FR').date_formats['long'] + + + :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'] + + >>> Locale('fr', 'FR').time_formats['long'] + + + :type: `dict` + """) + + def datetime_formats(self): + return self._data['datetime_formats'] + datetime_formats = property(datetime_formats, doc="""\ + Locale patterns for datetime formatting. + + >>> Locale('en').datetime_formats[None] + u'{1} {0}' + >>> Locale('th').datetime_formats[None] + u'{1}, {0}' + + :type: `dict` + """) + + +def default_locale(category=None, aliases=LOCALE_ALIASES): + """Returns the system default locale for a given category, based on + environment variables. + + >>> for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE']: + ... os.environ[name] = '' + >>> os.environ['LANG'] = 'fr_FR.UTF-8' + >>> default_locale('LC_MESSAGES') + 'fr_FR' + + The "C" or "POSIX" pseudo-locales are treated as aliases for the + "en_US_POSIX" locale: + + >>> os.environ['LC_MESSAGES'] = 'POSIX' + >>> default_locale('LC_MESSAGES') + 'en_US_POSIX' + + :param category: one of the ``LC_XXX`` environment variable names + :param aliases: a dictionary of aliases for locale identifiers + :return: the value of the variable, or any of the fallbacks (``LANGUAGE``, + ``LC_ALL``, ``LC_CTYPE``, and ``LANG``) + :rtype: `str` + """ + varnames = (category, 'LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG') + for name in filter(None, varnames): + locale = os.getenv(name) + if locale: + if name == 'LANGUAGE' and ':' in locale: + # the LANGUAGE variable may contain a colon-separated list of + # language codes; we just pick the language on the list + locale = locale.split(':')[0] + if locale in ('C', 'POSIX'): + locale = 'en_US_POSIX' + elif aliases and locale in aliases: + locale = aliases[locale] + try: + return '_'.join(filter(None, parse_locale(locale))) + except ValueError: + pass + +def negotiate_locale(preferred, available, sep='_', aliases=LOCALE_ALIASES): + """Find the best match between available and requested locale strings. + + >>> negotiate_locale(['de_DE', 'en_US'], ['de_DE', 'de_AT']) + 'de_DE' + >>> negotiate_locale(['de_DE', 'en_US'], ['en', 'de']) + 'de' + + Case is ignored by the algorithm, the result uses the case of the preferred + locale identifier: + + >>> negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at']) + 'de_DE' + + >>> negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at']) + 'de_DE' + + By default, some web browsers unfortunately do not include the territory + in the locale identifier for many locales, and some don't even allow the + user to easily add the territory. So while you may prefer using qualified + locale identifiers in your web-application, they would not normally match + the language-only locale sent by such browsers. To workaround that, this + function uses a default mapping of commonly used langauge-only locale + identifiers to identifiers including the territory: + + >>> negotiate_locale(['ja', 'en_US'], ['ja_JP', 'en_US']) + 'ja_JP' + + Some browsers even use an incorrect or outdated language code, such as "no" + for Norwegian, where the correct locale identifier would actually be "nb_NO" + (Bokmål) or "nn_NO" (Nynorsk). The aliases are intended to take care of + such cases, too: + + >>> negotiate_locale(['no', 'sv'], ['nb_NO', 'sv_SE']) + 'nb_NO' + + You can override this default mapping by passing a different `aliases` + dictionary to this function, or you can bypass the behavior althogher by + setting the `aliases` parameter to `None`. + + :param preferred: the list of locale strings preferred by the user + :param available: the list of locale strings available + :param sep: character that separates the different parts of the locale + strings + :param aliases: a dictionary of aliases for locale identifiers + :return: the locale identifier for the best match, or `None` if no match + was found + :rtype: `str` + """ + available = [a.lower() for a in available if a] + for locale in preferred: + ll = locale.lower() + if ll in available: + return locale + if aliases: + alias = aliases.get(ll) + if alias: + alias = alias.replace('_', sep) + if alias.lower() in available: + return alias + parts = locale.split(sep) + if len(parts) > 1 and parts[0].lower() in available: + return parts[0] + return None + +def parse_locale(identifier, sep='_'): + """Parse a locale identifier into a tuple of the form:: + + ``(language, territory, script, variant)`` + + >>> parse_locale('zh_CN') + ('zh', 'CN', None, None) + >>> parse_locale('zh_Hans_CN') + ('zh', 'CN', 'Hans', None) + + The default component separator is "_", but a different separator can be + specified using the `sep` parameter: + + >>> parse_locale('zh-CN', sep='-') + ('zh', 'CN', None, None) + + If the identifier cannot be parsed into a locale, a `ValueError` exception + is raised: + + >>> parse_locale('not_a_LOCALE_String') + Traceback (most recent call last): + ... + ValueError: 'not_a_LOCALE_String' is not a valid locale identifier + + Encoding information and locale modifiers are removed from the identifier: + + >>> parse_locale('it_IT@euro') + ('it', 'IT', None, None) + >>> parse_locale('en_US.UTF-8') + ('en', 'US', None, None) + >>> parse_locale('de_DE.iso885915@euro') + ('de', 'DE', None, None) + + :param identifier: the locale identifier string + :param sep: character that separates the different components of the locale + identifier + :return: the ``(language, territory, script, variant)`` tuple + :rtype: `tuple` + :raise `ValueError`: if the string does not appear to be a valid locale + identifier + + :see: `IETF RFC 4646 `_ + """ + if '.' in identifier: + # this is probably the charset/encoding, which we don't care about + identifier = identifier.split('.', 1)[0] + if '@' in identifier: + # this is a locale modifier such as @euro, which we don't care about + # either + identifier = identifier.split('@', 1)[0] + + parts = identifier.split(sep) + lang = parts.pop(0).lower() + if not lang.isalpha(): + raise ValueError('expected only letters, got %r' % lang) + + script = territory = variant = None + if parts: + if len(parts[0]) == 4 and parts[0].isalpha(): + script = parts.pop(0).title() + + if parts: + if len(parts[0]) == 2 and parts[0].isalpha(): + territory = parts.pop(0).upper() + elif len(parts[0]) == 3 and parts[0].isdigit(): + territory = parts.pop(0) + + if parts: + if len(parts[0]) == 4 and parts[0][0].isdigit() or \ + len(parts[0]) >= 5 and parts[0][0].isalpha(): + variant = parts.pop() + + if parts: + raise ValueError('%r is not a valid locale identifier' % identifier) + + return lang, territory, script, variant diff --git a/justice/external/babel/dates.py b/justice/external/babel/dates.py index fa14f9e..a8ac0f1 100644 --- a/justice/external/babel/dates.py +++ b/justice/external/babel/dates.py @@ -1,991 +1,991 @@ -# -*- 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, timedelta, tzinfo -import re - -from babel.core import default_locale, get_global, Locale -from babel.util import UTC - -__all__ = ['format_date', 'format_datetime', 'format_time', - 'get_timezone_name', 'parse_date', 'parse_datetime', 'parse_time'] -__docformat__ = 'restructuredtext en' - -LC_TIME = default_locale('LC_TIME') - -# Aliases for use in scopes where the modules are shadowed by local variables -date_ = date -datetime_ = datetime -time_ = 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'Tuesday' - >>> get_day_names('abbreviated', locale='es')[1] - u'mar' - >>> get_day_names('narrow', context='stand-alone', locale='de_DE')[1] - u'D' - - :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", "abbreviated", or "narrow" - :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') - - >>> get_date_format('full', locale='de_DE') - - - :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: `DateTimePattern` - """ - return Locale.parse(locale).date_formats[format] - -def get_datetime_format(format='medium', locale=LC_TIME): - """Return the datetime formatting patterns used by the locale for the - specified format. - - >>> get_datetime_format(locale='en_US') - u'{1} {0}' - - :param format: the format to use, one of "full", "long", "medium", or - "short" - :param locale: the `Locale` object, or a locale string - :return: the datetime format pattern - :rtype: `unicode` - """ - patterns = Locale.parse(locale).datetime_formats - if format not in patterns: - format = None - return patterns[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') - - >>> get_time_format('full', locale='de_DE') - - - :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: `DateTimePattern` - """ - return Locale.parse(locale).time_formats[format] - -def get_timezone_gmt(datetime=None, width='long', locale=LC_TIME): - """Return the timezone associated with the given `datetime` object formatted - as string indicating the offset from GMT. - - >>> dt = datetime(2007, 4, 1, 15, 30) - >>> get_timezone_gmt(dt, locale='en') - u'GMT+00:00' - - >>> from pytz import timezone - >>> tz = timezone('America/Los_Angeles') - >>> dt = datetime(2007, 4, 1, 15, 30, tzinfo=tz) - >>> get_timezone_gmt(dt, locale='en') - u'GMT-08:00' - >>> get_timezone_gmt(dt, 'short', locale='en') - u'-0800' - - The long format depends on the locale, for example in France the acronym - UTC string is used instead of GMT: - - >>> get_timezone_gmt(dt, 'long', locale='fr_FR') - u'UTC-08:00' - - :param datetime: the ``datetime`` object; if `None`, the current date and - time in UTC is used - :param width: either "long" or "short" - :param locale: the `Locale` object, or a locale string - :return: the GMT offset representation of the timezone - :rtype: `unicode` - :since: version 0.9 - """ - if datetime is None: - datetime = datetime_.utcnow() - elif isinstance(datetime, (int, long)): - datetime = datetime_.utcfromtimestamp(datetime).time() - if datetime.tzinfo is None: - datetime = datetime.replace(tzinfo=UTC) - locale = Locale.parse(locale) - - offset = datetime.tzinfo.utcoffset(datetime) - seconds = offset.days * 24 * 60 * 60 + offset.seconds - hours, seconds = divmod(seconds, 3600) - if width == 'short': - pattern = u'%+03d%02d' - else: - pattern = locale.zone_formats['gmt'] % '%+03d:%02d' - return pattern % (hours, seconds // 60) - -def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME): - """Return a representation of the given timezone using "location format". - - The result depends on both the local display name of the country and the - city associated with the time zone: - - >>> from pytz import timezone - >>> tz = timezone('America/St_Johns') - >>> get_timezone_location(tz, locale='de_DE') - u"Kanada (St. John's)" - >>> tz = timezone('America/Mexico_City') - >>> get_timezone_location(tz, locale='de_DE') - u'Mexiko (Mexiko-Stadt)' - - If the timezone is associated with a country that uses only a single - timezone, just the localized country name is returned: - - >>> tz = timezone('Europe/Berlin') - >>> get_timezone_name(tz, locale='de_DE') - u'Deutschland' - - :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines - the timezone; if `None`, the current date and time in - UTC is assumed - :param locale: the `Locale` object, or a locale string - :return: the localized timezone name using location format - :rtype: `unicode` - :since: version 0.9 - """ - if dt_or_tzinfo is None or isinstance(dt_or_tzinfo, (int, long)): - dt = None - tzinfo = UTC - elif isinstance(dt_or_tzinfo, (datetime, time)): - dt = dt_or_tzinfo - if dt.tzinfo is not None: - tzinfo = dt.tzinfo - else: - tzinfo = UTC - else: - dt = None - tzinfo = dt_or_tzinfo - locale = Locale.parse(locale) - - if hasattr(tzinfo, 'zone'): - zone = tzinfo.zone - else: - zone = tzinfo.tzname(dt or datetime.utcnow()) - - # Get the canonical time-zone code - zone = get_global('zone_aliases').get(zone, zone) - - info = locale.time_zones.get(zone, {}) - - # Otherwise, if there is only one timezone for the country, return the - # localized country name - region_format = locale.zone_formats['region'] - territory = get_global('zone_territories').get(zone) - if territory not in locale.territories: - territory = 'ZZ' # invalid/unknown - territory_name = locale.territories[territory] - if territory and len(get_global('territory_zones').get(territory, [])) == 1: - return region_format % (territory_name) - - # Otherwise, include the city in the output - fallback_format = locale.zone_formats['fallback'] - if 'city' in info: - city_name = info['city'] - else: - metazone = get_global('meta_zones').get(zone) - metazone_info = locale.meta_zones.get(metazone, {}) - if 'city' in metazone_info: - city_name = metainfo['city'] - elif '/' in zone: - city_name = zone.split('/', 1)[1].replace('_', ' ') - else: - city_name = zone.replace('_', ' ') - - return region_format % (fallback_format % { - '0': city_name, - '1': territory_name - }) - -def get_timezone_name(dt_or_tzinfo=None, width='long', uncommon=False, - locale=LC_TIME): - r"""Return the localized display name for the given timezone. The timezone - may be specified using a ``datetime`` or `tzinfo` object. - - >>> from pytz import timezone - >>> dt = time(15, 30, tzinfo=timezone('America/Los_Angeles')) - >>> get_timezone_name(dt, locale='en_US') - u'Pacific Standard Time' - >>> get_timezone_name(dt, width='short', locale='en_US') - u'PST' - - If this function gets passed only a `tzinfo` object and no concrete - `datetime`, the returned display name is indenpendent of daylight savings - time. This can be used for example for selecting timezones, or to set the - time of events that recur across DST changes: - - >>> tz = timezone('America/Los_Angeles') - >>> get_timezone_name(tz, locale='en_US') - u'Pacific Time' - >>> get_timezone_name(tz, 'short', locale='en_US') - u'PT' - - If no localized display name for the timezone is available, and the timezone - is associated with a country that uses only a single timezone, the name of - that country is returned, formatted according to the locale: - - >>> tz = timezone('Europe/Berlin') - >>> get_timezone_name(tz, locale='de_DE') - u'Deutschland' - >>> get_timezone_name(tz, locale='pt_BR') - u'Hor\xe1rio Alemanha' - - On the other hand, if the country uses multiple timezones, the city is also - included in the representation: - - >>> tz = timezone('America/St_Johns') - >>> get_timezone_name(tz, locale='de_DE') - u"Kanada (St. John's)" - - The `uncommon` parameter can be set to `True` to enable the use of timezone - representations that are not commonly used by the requested locale. For - example, while in French the central European timezone is usually - abbreviated as "HEC", in Canadian French, this abbreviation is not in - common use, so a generic name would be chosen by default: - - >>> tz = timezone('Europe/Paris') - >>> get_timezone_name(tz, 'short', locale='fr_CA') - u'France' - >>> get_timezone_name(tz, 'short', uncommon=True, locale='fr_CA') - u'HEC' - - :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines - the timezone; if a ``tzinfo`` object is used, the - resulting display name will be generic, i.e. - independent of daylight savings time; if `None`, the - current date in UTC is assumed - :param width: either "long" or "short" - :param uncommon: whether even uncommon timezone abbreviations should be used - :param locale: the `Locale` object, or a locale string - :return: the timezone display name - :rtype: `unicode` - :since: version 0.9 - :see: `LDML Appendix J: Time Zone Display Names - `_ - """ - if dt_or_tzinfo is None or isinstance(dt_or_tzinfo, (int, long)): - dt = None - tzinfo = UTC - elif isinstance(dt_or_tzinfo, (datetime, time)): - dt = dt_or_tzinfo - if dt.tzinfo is not None: - tzinfo = dt.tzinfo - else: - tzinfo = UTC - else: - dt = None - tzinfo = dt_or_tzinfo - locale = Locale.parse(locale) - - if hasattr(tzinfo, 'zone'): - zone = tzinfo.zone - else: - zone = tzinfo.tzname(dt) - - # Get the canonical time-zone code - zone = get_global('zone_aliases').get(zone, zone) - - info = locale.time_zones.get(zone, {}) - # Try explicitly translated zone names first - if width in info: - if dt is None: - field = 'generic' - else: - dst = tzinfo.dst(dt) - if dst is None: - field = 'generic' - elif dst == 0: - field = 'standard' - else: - field = 'daylight' - if field in info[width]: - return info[width][field] - - metazone = get_global('meta_zones').get(zone) - if metazone: - metazone_info = locale.meta_zones.get(metazone, {}) - if width in metazone_info and (uncommon or metazone_info.get('common')): - if dt is None: - field = 'generic' - else: - field = tzinfo.dst(dt) and 'daylight' or 'standard' - if field in metazone_info[width]: - return metazone_info[width][field] - - # If we have a concrete datetime, we assume that the result can't be - # independent of daylight savings time, so we return the GMT offset - if dt is not None: - return get_timezone_gmt(dt, width=width, locale=locale) - - return get_timezone_location(dt_or_tzinfo, locale=locale) - -def format_date(date=None, format='medium', locale=LC_TIME): - """Return 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' - - If you don't want to use the locale default formats, you can specify a - custom date pattern: - - >>> format_date(d, "EEE, MMM d, ''yy", locale='en') - u"Sun, Apr 1, '07" - - :param date: the ``date`` or ``datetime`` object; if `None`, the current - date is used - :param format: one of "full", "long", "medium", or "short", or a custom - date/time pattern - :param locale: a `Locale` object or a locale identifier - :rtype: `unicode` - - :note: If the pattern contains time fields, an `AttributeError` will be - raised when trying to apply the formatting. This is also true if - the value of ``date`` parameter is actually a ``datetime`` object, - as this function automatically converts that to a ``date``. - """ - if date is None: - date = date_.today() - elif isinstance(date, datetime): - date = date.date() - - locale = Locale.parse(locale) - if format in ('full', 'long', 'medium', 'short'): - format = get_date_format(format, locale=locale) - pattern = parse_pattern(format) - return pattern.apply(date, locale) - -def format_datetime(datetime=None, format='medium', tzinfo=None, - locale=LC_TIME): - """Return a date formatted according to the given pattern. - - >>> dt = datetime(2007, 04, 01, 15, 30) - >>> format_datetime(dt, locale='en_US') - u'Apr 1, 2007 3:30:00 PM' - - For any pattern requiring the display of the time-zone, the third-party - ``pytz`` package is needed to explicitly specify the time-zone: - - >>> from pytz import timezone - >>> format_datetime(dt, 'full', tzinfo=timezone('Europe/Paris'), - ... locale='fr_FR') - u'dimanche 1 avril 2007 17:30:00 HEC' - >>> format_datetime(dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz", - ... tzinfo=timezone('US/Eastern'), locale='en') - u'2007.04.01 AD at 11:30:00 EDT' - - :param datetime: the `datetime` object; if `None`, the current date and - time is used - :param format: one of "full", "long", "medium", or "short", or a custom - date/time pattern - :param tzinfo: the timezone to apply to the time for display - :param locale: a `Locale` object or a locale identifier - :rtype: `unicode` - """ - if datetime is None: - datetime = datetime_.utcnow() - elif isinstance(datetime, (int, long)): - datetime = datetime_.utcfromtimestamp(datetime) - elif isinstance(datetime, time): - datetime = datetime_.combine(date.today(), datetime) - if datetime.tzinfo is None: - datetime = datetime.replace(tzinfo=UTC) - if tzinfo is not None: - datetime = datetime.astimezone(tzinfo) - if hasattr(tzinfo, 'normalize'): # pytz - datetime = tzinfo.normalize(datetime) - - locale = Locale.parse(locale) - if format in ('full', 'long', 'medium', 'short'): - return get_datetime_format(format, locale=locale) \ - .replace('{0}', format_time(datetime, format, tzinfo=None, - locale=locale)) \ - .replace('{1}', format_date(datetime, format, locale=locale)) - else: - return parse_pattern(format).apply(datetime, locale) - -def format_time(time=None, format='medium', tzinfo=None, locale=LC_TIME): - """Return 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' - - If you don't want to use the locale default formats, you can specify a - custom time pattern: - - >>> format_time(t, "hh 'o''clock' a", locale='en') - u"03 o'clock PM" - - For any pattern requiring the display of the time-zone, the third-party - ``pytz`` package is needed to explicitly specify the time-zone: - - >>> from pytz import timezone - >>> t = datetime(2007, 4, 1, 15, 30) - >>> tzinfo = timezone('Europe/Paris') - >>> t = tzinfo.localize(t) - >>> format_time(t, format='full', tzinfo=tzinfo, locale='fr_FR') - u'15:30:00 HEC' - >>> format_time(t, "hh 'o''clock' a, zzzz", tzinfo=timezone('US/Eastern'), - ... locale='en') - u"09 o'clock AM, Eastern Daylight Time" - - As that example shows, when this function gets passed a - ``datetime.datetime`` value, the actual time in the formatted string is - adjusted to the timezone specified by the `tzinfo` parameter. If the - ``datetime`` is "naive" (i.e. it has no associated timezone information), - it is assumed to be in UTC. - - These timezone calculations are **not** performed if the value is of type - ``datetime.time``, as without date information there's no way to determine - what a given time would translate to in a different timezone without - information about whether daylight savings time is in effect or not. This - means that time values are left as-is, and the value of the `tzinfo` - parameter is only used to display the timezone name if needed: - - >>> t = time(15, 30) - >>> format_time(t, format='full', tzinfo=timezone('Europe/Paris'), - ... locale='fr_FR') - u'15:30:00 HEC' - >>> format_time(t, format='full', tzinfo=timezone('US/Eastern'), - ... locale='en_US') - u'3:30:00 PM ET' - - :param time: the ``time`` or ``datetime`` object; if `None`, the current - time in UTC is used - :param format: one of "full", "long", "medium", or "short", or a custom - date/time pattern - :param tzinfo: the time-zone to apply to the time for display - :param locale: a `Locale` object or a locale identifier - :rtype: `unicode` - - :note: If the pattern contains date fields, an `AttributeError` will be - raised when trying to apply the formatting. This is also true if - the value of ``time`` parameter is actually a ``datetime`` object, - as this function automatically converts that to a ``time``. - """ - if time is None: - time = datetime.utcnow() - elif isinstance(time, (int, long)): - time = datetime.utcfromtimestamp(time) - if time.tzinfo is None: - time = time.replace(tzinfo=UTC) - if isinstance(time, datetime): - if tzinfo is not None: - time = time.astimezone(tzinfo) - if hasattr(tzinfo, 'normalize'): # pytz - time = tzinfo.normalize(time) - time = time.timetz() - elif tzinfo is not None: - time = time.replace(tzinfo=tzinfo) - - 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): - """Parse a date from a string. - - This function uses the date format for the locale as a hint to determine - the order in which the date fields appear in the string. - - >>> parse_date('4/1/04', locale='en_US') - datetime.date(2004, 4, 1) - >>> parse_date('01.04.2004', locale='de_DE') - datetime.date(2004, 4, 1) - - :param string: the string containing the date - :param locale: a `Locale` object or a locale identifier - :return: the parsed date - :rtype: `date` - """ - # TODO: try ISO format first? - format = get_date_format(locale=locale).pattern.lower() - year_idx = format.index('y') - month_idx = format.index('m') - if month_idx < 0: - month_idx = format.index('l') - day_idx = format.index('d') - - indexes = [(year_idx, 'Y'), (month_idx, 'M'), (day_idx, 'D')] - indexes.sort() - indexes = dict([(item[1], idx) for idx, item in enumerate(indexes)]) - - # FIXME: this currently only supports numbers, but should also support month - # names, both in the requested locale, and english - - numbers = re.findall('(\d+)', string) - year = numbers[indexes['Y']] - if len(year) == 2: - year = 2000 + int(year) - else: - year = int(year) - month = int(numbers[indexes['M']]) - day = int(numbers[indexes['D']]) - if month > 12: - month, day = day, month - return date(year, month, day) - -def parse_datetime(string, locale=LC_TIME): - """Parse a date and time from a string. - - This function uses the date and time formats for the locale as a hint to - determine the order in which the time fields appear in the string. - - :param string: the string containing the date and time - :param locale: a `Locale` object or a locale identifier - :return: the parsed date/time - :rtype: `datetime` - """ - raise NotImplementedError - -def parse_time(string, locale=LC_TIME): - """Parse a time from a string. - - This function uses the time format for the locale as a hint to determine - the order in which the time fields appear in the string. - - >>> parse_time('15:30:00', locale='en_US') - datetime.time(15, 30) - - :param string: the string containing the time - :param locale: a `Locale` object or a locale identifier - :return: the parsed time - :rtype: `time` - """ - # TODO: try ISO format first? - format = get_time_format(locale=locale).pattern.lower() - hour_idx = format.index('h') - if hour_idx < 0: - hour_idx = format.index('k') - min_idx = format.index('m') - sec_idx = format.index('s') - - indexes = [(hour_idx, 'H'), (min_idx, 'M'), (sec_idx, 'S')] - indexes.sort() - indexes = dict([(item[1], idx) for idx, item in enumerate(indexes)]) - - # FIXME: support 12 hour clock, and 0-based hour specification - # and seconds should be optional, maybe minutes too - # oh, and time-zones, of course - - numbers = re.findall('(\d+)', string) - hour = int(numbers[indexes['H']]) - minute = int(numbers[indexes['M']]) - second = int(numbers[indexes['S']]) - return time(hour, minute, second) - - -class DateTimePattern(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)) - if isinstance(value, (datetime, time)) and value.tzinfo is None: - value = value.replace(tzinfo=UTC) - self.value = value - self.locale = Locale.parse(locale) - - def __getitem__(self, name): - char = name[0] - num = len(name) - if char == 'G': - return self.format_era(char, num) - elif char in ('y', 'Y', 'u'): - return self.format_year(char, num) - elif char in ('Q', 'q'): - return self.format_quarter(char, num) - elif char in ('M', 'L'): - return self.format_month(char, num) - elif char in ('w', 'W'): - return self.format_week(char, num) - elif char == 'd': - return self.format(self.value.day, num) - elif char == 'D': - return self.format_day_of_year(num) - elif char == 'F': - return self.format_day_of_week_in_month() - elif char in ('E', 'e', 'c'): - return self.format_weekday(char, num) - elif char == 'a': - return self.format_period(char) - elif char == 'h': - if self.value.hour % 12 == 0: - return self.format(12, num) - else: - return self.format(self.value.hour % 12, num) - elif char == 'H': - return self.format(self.value.hour, num) - elif char == 'K': - return self.format(self.value.hour % 12, num) - elif char == 'k': - if self.value.hour == 0: - return self.format(24, num) - else: - return self.format(self.value.hour, num) - elif char == 'm': - return self.format(self.value.minute, num) - elif char == 's': - return self.format(self.value.second, num) - elif char == 'S': - return self.format_frac_seconds(num) - elif char == 'A': - return self.format_milliseconds_in_day(num) - elif char in ('z', 'Z', 'v', 'V'): - return self.format_timezone(char, num) - else: - raise KeyError('Unsupported date/time field %r' % char) - - def format_era(self, char, 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, char, num): - value = self.value.year - if char.isupper(): - week = self.get_week_number(self.get_day_of_year()) - if week == 0: - value -= 1 - year = self.format(value, num) - if num == 2: - year = year[-2:] - return year - - def format_quarter(self, char, num): - quarter = (self.value.month - 1) // 3 + 1 - if num <= 2: - return ('%%0%dd' % num) % quarter - width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] - context = {'Q': 'format', 'q': 'stand-alone'}[char] - return get_quarter_names(width, context, self.locale)[quarter] - - def format_month(self, char, num): - if num <= 2: - return ('%%0%dd' % num) % self.value.month - width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] - context = {'M': 'format', 'L': 'stand-alone'}[char] - return get_month_names(width, context, self.locale)[self.value.month] - - def format_week(self, char, num): - if char.islower(): # week of year - day_of_year = self.get_day_of_year() - week = self.get_week_number(day_of_year) - if week == 0: - date = self.value - timedelta(days=day_of_year) - week = self.get_week_number(self.get_day_of_year(date), - date.weekday()) - return self.format(week, num) - else: # week of month - week = self.get_week_number(self.value.day) - if week == 0: - date = self.value - timedelta(days=self.value.day) - week = self.get_week_number(date.day, date.weekday()) - pass - return '%d' % week - - def format_weekday(self, char, num): - if num < 3: - if char.islower(): - value = 7 - self.locale.first_week_day + self.value.weekday() - return self.format(value % 7 + 1, num) - num = 3 - weekday = self.value.weekday() - width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] - context = {3: 'format', 4: 'format', 5: 'stand-alone'}[num] - return get_day_names(width, context, self.locale)[weekday] - - def format_day_of_year(self, num): - return self.format(self.get_day_of_year(), num) - - def format_day_of_week_in_month(self): - return '%d' % ((self.value.day - 1) / 7 + 1) - - def format_period(self, char): - period = {0: 'am', 1: 'pm'}[int(self.value.hour >= 12)] - return get_period_names(locale=self.locale)[period] - - def format_frac_seconds(self, num): - value = str(self.value.microsecond) - return self.format(round(float('.%s' % value), num) * 10**num, num) - - def format_milliseconds_in_day(self, num): - msecs = self.value.microsecond // 1000 + self.value.second * 1000 + \ - self.value.minute * 60000 + self.value.hour * 3600000 - return self.format(msecs, num) - - def format_timezone(self, char, num): - width = {3: 'short', 4: 'long'}[max(3, num)] - if char == 'z': - return get_timezone_name(self.value, width, locale=self.locale) - elif char == 'Z': - return get_timezone_gmt(self.value, width, locale=self.locale) - elif char == 'v': - return get_timezone_name(self.value.tzinfo, width, - locale=self.locale) - elif char == 'V': - if num == 1: - return get_timezone_name(self.value.tzinfo, width, - uncommon=True, locale=self.locale) - return get_timezone_location(self.value.tzinfo, locale=self.locale) - - def format(self, value, length): - return ('%%0%dd' % length) % value - - def get_day_of_year(self, date=None): - if date is None: - date = self.value - return (date - date_(date.year, 1, 1)).days + 1 - - def get_week_number(self, day_of_period, day_of_week=None): - """Return the number of the week of a day within a period. This may be - the week number in a year or the week number in a month. - - Usually this will return a value equal to or greater than 1, but if the - first week of the period is so short that it actually counts as the last - week of the previous period, this function will return 0. - - >>> format = DateTimeFormat(date(2006, 1, 8), Locale.parse('de_DE')) - >>> format.get_week_number(6) - 1 - - >>> format = DateTimeFormat(date(2006, 1, 8), Locale.parse('en_US')) - >>> format.get_week_number(6) - 2 - - :param day_of_period: the number of the day in the period (usually - either the day of month or the day of year) - :param day_of_week: the week day; if ommitted, the week day of the - current date is assumed - """ - if day_of_week is None: - day_of_week = self.value.weekday() - first_day = (day_of_week - self.locale.first_week_day - - day_of_period + 1) % 7 - if first_day < 0: - first_day += 7 - week_number = (day_of_period + first_day - 1) / 7 - if 7 - first_day >= self.locale.min_week_days: - week_number += 1 - return week_number - - -PATTERN_CHARS = { - 'G': [1, 2, 3, 4, 5], # era - 'y': None, 'Y': None, 'u': None, # year - 'Q': [1, 2, 3, 4], 'q': [1, 2, 3, 4], # quarter - 'M': [1, 2, 3, 4, 5], 'L': [1, 2, 3, 4, 5], # month - 'w': [1, 2], 'W': [1], # week - 'd': [1, 2], 'D': [1, 2, 3], 'F': [1], 'g': None, # day - 'E': [1, 2, 3, 4, 5], 'e': [1, 2, 3, 4, 5], 'c': [1, 3, 4, 5], # week day - 'a': [1], # period - 'h': [1, 2], 'H': [1, 2], 'K': [1, 2], 'k': [1, 2], # hour - 'm': [1, 2], # minute - 's': [1, 2], 'S': None, 'A': None, # second - 'z': [1, 2, 3, 4], 'Z': [1, 2, 3, 4], 'v': [1, 4], 'V': [1, 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' - - Pattern can contain literal strings in single quotes: - - >>> parse_pattern("H:mm' Uhr 'z").format - u'%(H)s:%(mm)s Uhr %(z)s' - - An actual single quote can be used by using two adjacent single quote - characters: - - >>> parse_pattern("hh' o''clock'").format - u"%(hh)s o'clock" - - :param pattern: the formatting pattern to parse - """ - if type(pattern) is DateTimePattern: - 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 and fieldnum[0] not in 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.replace("''", '\0')): - 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 == "'": # end of quote - charbuf.extend(quotebuf) - quotebuf = None - else: # inside quote - quotebuf.append(char) - - if fieldchar[0]: - append_field() - elif charbuf: - append_chars() - - return DateTimePattern(pattern, u''.join(result).replace('\0', "'")) +# -*- 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, timedelta, tzinfo +import re + +from babel.core import default_locale, get_global, Locale +from babel.util import UTC + +__all__ = ['format_date', 'format_datetime', 'format_time', + 'get_timezone_name', 'parse_date', 'parse_datetime', 'parse_time'] +__docformat__ = 'restructuredtext en' + +LC_TIME = default_locale('LC_TIME') + +# Aliases for use in scopes where the modules are shadowed by local variables +date_ = date +datetime_ = datetime +time_ = 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'Tuesday' + >>> get_day_names('abbreviated', locale='es')[1] + u'mar' + >>> get_day_names('narrow', context='stand-alone', locale='de_DE')[1] + u'D' + + :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", "abbreviated", or "narrow" + :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') + + >>> get_date_format('full', locale='de_DE') + + + :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: `DateTimePattern` + """ + return Locale.parse(locale).date_formats[format] + +def get_datetime_format(format='medium', locale=LC_TIME): + """Return the datetime formatting patterns used by the locale for the + specified format. + + >>> get_datetime_format(locale='en_US') + u'{1} {0}' + + :param format: the format to use, one of "full", "long", "medium", or + "short" + :param locale: the `Locale` object, or a locale string + :return: the datetime format pattern + :rtype: `unicode` + """ + patterns = Locale.parse(locale).datetime_formats + if format not in patterns: + format = None + return patterns[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') + + >>> get_time_format('full', locale='de_DE') + + + :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: `DateTimePattern` + """ + return Locale.parse(locale).time_formats[format] + +def get_timezone_gmt(datetime=None, width='long', locale=LC_TIME): + """Return the timezone associated with the given `datetime` object formatted + as string indicating the offset from GMT. + + >>> dt = datetime(2007, 4, 1, 15, 30) + >>> get_timezone_gmt(dt, locale='en') + u'GMT+00:00' + + >>> from pytz import timezone + >>> tz = timezone('America/Los_Angeles') + >>> dt = datetime(2007, 4, 1, 15, 30, tzinfo=tz) + >>> get_timezone_gmt(dt, locale='en') + u'GMT-08:00' + >>> get_timezone_gmt(dt, 'short', locale='en') + u'-0800' + + The long format depends on the locale, for example in France the acronym + UTC string is used instead of GMT: + + >>> get_timezone_gmt(dt, 'long', locale='fr_FR') + u'UTC-08:00' + + :param datetime: the ``datetime`` object; if `None`, the current date and + time in UTC is used + :param width: either "long" or "short" + :param locale: the `Locale` object, or a locale string + :return: the GMT offset representation of the timezone + :rtype: `unicode` + :since: version 0.9 + """ + if datetime is None: + datetime = datetime_.utcnow() + elif isinstance(datetime, (int, long)): + datetime = datetime_.utcfromtimestamp(datetime).time() + if datetime.tzinfo is None: + datetime = datetime.replace(tzinfo=UTC) + locale = Locale.parse(locale) + + offset = datetime.tzinfo.utcoffset(datetime) + seconds = offset.days * 24 * 60 * 60 + offset.seconds + hours, seconds = divmod(seconds, 3600) + if width == 'short': + pattern = u'%+03d%02d' + else: + pattern = locale.zone_formats['gmt'] % '%+03d:%02d' + return pattern % (hours, seconds // 60) + +def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME): + """Return a representation of the given timezone using "location format". + + The result depends on both the local display name of the country and the + city associated with the time zone: + + >>> from pytz import timezone + >>> tz = timezone('America/St_Johns') + >>> get_timezone_location(tz, locale='de_DE') + u"Kanada (St. John's)" + >>> tz = timezone('America/Mexico_City') + >>> get_timezone_location(tz, locale='de_DE') + u'Mexiko (Mexiko-Stadt)' + + If the timezone is associated with a country that uses only a single + timezone, just the localized country name is returned: + + >>> tz = timezone('Europe/Berlin') + >>> get_timezone_name(tz, locale='de_DE') + u'Deutschland' + + :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines + the timezone; if `None`, the current date and time in + UTC is assumed + :param locale: the `Locale` object, or a locale string + :return: the localized timezone name using location format + :rtype: `unicode` + :since: version 0.9 + """ + if dt_or_tzinfo is None or isinstance(dt_or_tzinfo, (int, long)): + dt = None + tzinfo = UTC + elif isinstance(dt_or_tzinfo, (datetime, time)): + dt = dt_or_tzinfo + if dt.tzinfo is not None: + tzinfo = dt.tzinfo + else: + tzinfo = UTC + else: + dt = None + tzinfo = dt_or_tzinfo + locale = Locale.parse(locale) + + if hasattr(tzinfo, 'zone'): + zone = tzinfo.zone + else: + zone = tzinfo.tzname(dt or datetime.utcnow()) + + # Get the canonical time-zone code + zone = get_global('zone_aliases').get(zone, zone) + + info = locale.time_zones.get(zone, {}) + + # Otherwise, if there is only one timezone for the country, return the + # localized country name + region_format = locale.zone_formats['region'] + territory = get_global('zone_territories').get(zone) + if territory not in locale.territories: + territory = 'ZZ' # invalid/unknown + territory_name = locale.territories[territory] + if territory and len(get_global('territory_zones').get(territory, [])) == 1: + return region_format % (territory_name) + + # Otherwise, include the city in the output + fallback_format = locale.zone_formats['fallback'] + if 'city' in info: + city_name = info['city'] + else: + metazone = get_global('meta_zones').get(zone) + metazone_info = locale.meta_zones.get(metazone, {}) + if 'city' in metazone_info: + city_name = metainfo['city'] + elif '/' in zone: + city_name = zone.split('/', 1)[1].replace('_', ' ') + else: + city_name = zone.replace('_', ' ') + + return region_format % (fallback_format % { + '0': city_name, + '1': territory_name + }) + +def get_timezone_name(dt_or_tzinfo=None, width='long', uncommon=False, + locale=LC_TIME): + r"""Return the localized display name for the given timezone. The timezone + may be specified using a ``datetime`` or `tzinfo` object. + + >>> from pytz import timezone + >>> dt = time(15, 30, tzinfo=timezone('America/Los_Angeles')) + >>> get_timezone_name(dt, locale='en_US') + u'Pacific Standard Time' + >>> get_timezone_name(dt, width='short', locale='en_US') + u'PST' + + If this function gets passed only a `tzinfo` object and no concrete + `datetime`, the returned display name is indenpendent of daylight savings + time. This can be used for example for selecting timezones, or to set the + time of events that recur across DST changes: + + >>> tz = timezone('America/Los_Angeles') + >>> get_timezone_name(tz, locale='en_US') + u'Pacific Time' + >>> get_timezone_name(tz, 'short', locale='en_US') + u'PT' + + If no localized display name for the timezone is available, and the timezone + is associated with a country that uses only a single timezone, the name of + that country is returned, formatted according to the locale: + + >>> tz = timezone('Europe/Berlin') + >>> get_timezone_name(tz, locale='de_DE') + u'Deutschland' + >>> get_timezone_name(tz, locale='pt_BR') + u'Hor\xe1rio Alemanha' + + On the other hand, if the country uses multiple timezones, the city is also + included in the representation: + + >>> tz = timezone('America/St_Johns') + >>> get_timezone_name(tz, locale='de_DE') + u"Kanada (St. John's)" + + The `uncommon` parameter can be set to `True` to enable the use of timezone + representations that are not commonly used by the requested locale. For + example, while in French the central European timezone is usually + abbreviated as "HEC", in Canadian French, this abbreviation is not in + common use, so a generic name would be chosen by default: + + >>> tz = timezone('Europe/Paris') + >>> get_timezone_name(tz, 'short', locale='fr_CA') + u'France' + >>> get_timezone_name(tz, 'short', uncommon=True, locale='fr_CA') + u'HEC' + + :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines + the timezone; if a ``tzinfo`` object is used, the + resulting display name will be generic, i.e. + independent of daylight savings time; if `None`, the + current date in UTC is assumed + :param width: either "long" or "short" + :param uncommon: whether even uncommon timezone abbreviations should be used + :param locale: the `Locale` object, or a locale string + :return: the timezone display name + :rtype: `unicode` + :since: version 0.9 + :see: `LDML Appendix J: Time Zone Display Names + `_ + """ + if dt_or_tzinfo is None or isinstance(dt_or_tzinfo, (int, long)): + dt = None + tzinfo = UTC + elif isinstance(dt_or_tzinfo, (datetime, time)): + dt = dt_or_tzinfo + if dt.tzinfo is not None: + tzinfo = dt.tzinfo + else: + tzinfo = UTC + else: + dt = None + tzinfo = dt_or_tzinfo + locale = Locale.parse(locale) + + if hasattr(tzinfo, 'zone'): + zone = tzinfo.zone + else: + zone = tzinfo.tzname(dt) + + # Get the canonical time-zone code + zone = get_global('zone_aliases').get(zone, zone) + + info = locale.time_zones.get(zone, {}) + # Try explicitly translated zone names first + if width in info: + if dt is None: + field = 'generic' + else: + dst = tzinfo.dst(dt) + if dst is None: + field = 'generic' + elif dst == 0: + field = 'standard' + else: + field = 'daylight' + if field in info[width]: + return info[width][field] + + metazone = get_global('meta_zones').get(zone) + if metazone: + metazone_info = locale.meta_zones.get(metazone, {}) + if width in metazone_info and (uncommon or metazone_info.get('common')): + if dt is None: + field = 'generic' + else: + field = tzinfo.dst(dt) and 'daylight' or 'standard' + if field in metazone_info[width]: + return metazone_info[width][field] + + # If we have a concrete datetime, we assume that the result can't be + # independent of daylight savings time, so we return the GMT offset + if dt is not None: + return get_timezone_gmt(dt, width=width, locale=locale) + + return get_timezone_location(dt_or_tzinfo, locale=locale) + +def format_date(date=None, format='medium', locale=LC_TIME): + """Return 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' + + If you don't want to use the locale default formats, you can specify a + custom date pattern: + + >>> format_date(d, "EEE, MMM d, ''yy", locale='en') + u"Sun, Apr 1, '07" + + :param date: the ``date`` or ``datetime`` object; if `None`, the current + date is used + :param format: one of "full", "long", "medium", or "short", or a custom + date/time pattern + :param locale: a `Locale` object or a locale identifier + :rtype: `unicode` + + :note: If the pattern contains time fields, an `AttributeError` will be + raised when trying to apply the formatting. This is also true if + the value of ``date`` parameter is actually a ``datetime`` object, + as this function automatically converts that to a ``date``. + """ + if date is None: + date = date_.today() + elif isinstance(date, datetime): + date = date.date() + + locale = Locale.parse(locale) + if format in ('full', 'long', 'medium', 'short'): + format = get_date_format(format, locale=locale) + pattern = parse_pattern(format) + return pattern.apply(date, locale) + +def format_datetime(datetime=None, format='medium', tzinfo=None, + locale=LC_TIME): + """Return a date formatted according to the given pattern. + + >>> dt = datetime(2007, 04, 01, 15, 30) + >>> format_datetime(dt, locale='en_US') + u'Apr 1, 2007 3:30:00 PM' + + For any pattern requiring the display of the time-zone, the third-party + ``pytz`` package is needed to explicitly specify the time-zone: + + >>> from pytz import timezone + >>> format_datetime(dt, 'full', tzinfo=timezone('Europe/Paris'), + ... locale='fr_FR') + u'dimanche 1 avril 2007 17:30:00 HEC' + >>> format_datetime(dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz", + ... tzinfo=timezone('US/Eastern'), locale='en') + u'2007.04.01 AD at 11:30:00 EDT' + + :param datetime: the `datetime` object; if `None`, the current date and + time is used + :param format: one of "full", "long", "medium", or "short", or a custom + date/time pattern + :param tzinfo: the timezone to apply to the time for display + :param locale: a `Locale` object or a locale identifier + :rtype: `unicode` + """ + if datetime is None: + datetime = datetime_.utcnow() + elif isinstance(datetime, (int, long)): + datetime = datetime_.utcfromtimestamp(datetime) + elif isinstance(datetime, time): + datetime = datetime_.combine(date.today(), datetime) + if datetime.tzinfo is None: + datetime = datetime.replace(tzinfo=UTC) + if tzinfo is not None: + datetime = datetime.astimezone(tzinfo) + if hasattr(tzinfo, 'normalize'): # pytz + datetime = tzinfo.normalize(datetime) + + locale = Locale.parse(locale) + if format in ('full', 'long', 'medium', 'short'): + return get_datetime_format(format, locale=locale) \ + .replace('{0}', format_time(datetime, format, tzinfo=None, + locale=locale)) \ + .replace('{1}', format_date(datetime, format, locale=locale)) + else: + return parse_pattern(format).apply(datetime, locale) + +def format_time(time=None, format='medium', tzinfo=None, locale=LC_TIME): + """Return 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' + + If you don't want to use the locale default formats, you can specify a + custom time pattern: + + >>> format_time(t, "hh 'o''clock' a", locale='en') + u"03 o'clock PM" + + For any pattern requiring the display of the time-zone, the third-party + ``pytz`` package is needed to explicitly specify the time-zone: + + >>> from pytz import timezone + >>> t = datetime(2007, 4, 1, 15, 30) + >>> tzinfo = timezone('Europe/Paris') + >>> t = tzinfo.localize(t) + >>> format_time(t, format='full', tzinfo=tzinfo, locale='fr_FR') + u'15:30:00 HEC' + >>> format_time(t, "hh 'o''clock' a, zzzz", tzinfo=timezone('US/Eastern'), + ... locale='en') + u"09 o'clock AM, Eastern Daylight Time" + + As that example shows, when this function gets passed a + ``datetime.datetime`` value, the actual time in the formatted string is + adjusted to the timezone specified by the `tzinfo` parameter. If the + ``datetime`` is "naive" (i.e. it has no associated timezone information), + it is assumed to be in UTC. + + These timezone calculations are **not** performed if the value is of type + ``datetime.time``, as without date information there's no way to determine + what a given time would translate to in a different timezone without + information about whether daylight savings time is in effect or not. This + means that time values are left as-is, and the value of the `tzinfo` + parameter is only used to display the timezone name if needed: + + >>> t = time(15, 30) + >>> format_time(t, format='full', tzinfo=timezone('Europe/Paris'), + ... locale='fr_FR') + u'15:30:00 HEC' + >>> format_time(t, format='full', tzinfo=timezone('US/Eastern'), + ... locale='en_US') + u'3:30:00 PM ET' + + :param time: the ``time`` or ``datetime`` object; if `None`, the current + time in UTC is used + :param format: one of "full", "long", "medium", or "short", or a custom + date/time pattern + :param tzinfo: the time-zone to apply to the time for display + :param locale: a `Locale` object or a locale identifier + :rtype: `unicode` + + :note: If the pattern contains date fields, an `AttributeError` will be + raised when trying to apply the formatting. This is also true if + the value of ``time`` parameter is actually a ``datetime`` object, + as this function automatically converts that to a ``time``. + """ + if time is None: + time = datetime.utcnow() + elif isinstance(time, (int, long)): + time = datetime.utcfromtimestamp(time) + if time.tzinfo is None: + time = time.replace(tzinfo=UTC) + if isinstance(time, datetime): + if tzinfo is not None: + time = time.astimezone(tzinfo) + if hasattr(tzinfo, 'normalize'): # pytz + time = tzinfo.normalize(time) + time = time.timetz() + elif tzinfo is not None: + time = time.replace(tzinfo=tzinfo) + + 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): + """Parse a date from a string. + + This function uses the date format for the locale as a hint to determine + the order in which the date fields appear in the string. + + >>> parse_date('4/1/04', locale='en_US') + datetime.date(2004, 4, 1) + >>> parse_date('01.04.2004', locale='de_DE') + datetime.date(2004, 4, 1) + + :param string: the string containing the date + :param locale: a `Locale` object or a locale identifier + :return: the parsed date + :rtype: `date` + """ + # TODO: try ISO format first? + format = get_date_format(locale=locale).pattern.lower() + year_idx = format.index('y') + month_idx = format.index('m') + if month_idx < 0: + month_idx = format.index('l') + day_idx = format.index('d') + + indexes = [(year_idx, 'Y'), (month_idx, 'M'), (day_idx, 'D')] + indexes.sort() + indexes = dict([(item[1], idx) for idx, item in enumerate(indexes)]) + + # FIXME: this currently only supports numbers, but should also support month + # names, both in the requested locale, and english + + numbers = re.findall('(\d+)', string) + year = numbers[indexes['Y']] + if len(year) == 2: + year = 2000 + int(year) + else: + year = int(year) + month = int(numbers[indexes['M']]) + day = int(numbers[indexes['D']]) + if month > 12: + month, day = day, month + return date(year, month, day) + +def parse_datetime(string, locale=LC_TIME): + """Parse a date and time from a string. + + This function uses the date and time formats for the locale as a hint to + determine the order in which the time fields appear in the string. + + :param string: the string containing the date and time + :param locale: a `Locale` object or a locale identifier + :return: the parsed date/time + :rtype: `datetime` + """ + raise NotImplementedError + +def parse_time(string, locale=LC_TIME): + """Parse a time from a string. + + This function uses the time format for the locale as a hint to determine + the order in which the time fields appear in the string. + + >>> parse_time('15:30:00', locale='en_US') + datetime.time(15, 30) + + :param string: the string containing the time + :param locale: a `Locale` object or a locale identifier + :return: the parsed time + :rtype: `time` + """ + # TODO: try ISO format first? + format = get_time_format(locale=locale).pattern.lower() + hour_idx = format.index('h') + if hour_idx < 0: + hour_idx = format.index('k') + min_idx = format.index('m') + sec_idx = format.index('s') + + indexes = [(hour_idx, 'H'), (min_idx, 'M'), (sec_idx, 'S')] + indexes.sort() + indexes = dict([(item[1], idx) for idx, item in enumerate(indexes)]) + + # FIXME: support 12 hour clock, and 0-based hour specification + # and seconds should be optional, maybe minutes too + # oh, and time-zones, of course + + numbers = re.findall('(\d+)', string) + hour = int(numbers[indexes['H']]) + minute = int(numbers[indexes['M']]) + second = int(numbers[indexes['S']]) + return time(hour, minute, second) + + +class DateTimePattern(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)) + if isinstance(value, (datetime, time)) and value.tzinfo is None: + value = value.replace(tzinfo=UTC) + self.value = value + self.locale = Locale.parse(locale) + + def __getitem__(self, name): + char = name[0] + num = len(name) + if char == 'G': + return self.format_era(char, num) + elif char in ('y', 'Y', 'u'): + return self.format_year(char, num) + elif char in ('Q', 'q'): + return self.format_quarter(char, num) + elif char in ('M', 'L'): + return self.format_month(char, num) + elif char in ('w', 'W'): + return self.format_week(char, num) + elif char == 'd': + return self.format(self.value.day, num) + elif char == 'D': + return self.format_day_of_year(num) + elif char == 'F': + return self.format_day_of_week_in_month() + elif char in ('E', 'e', 'c'): + return self.format_weekday(char, num) + elif char == 'a': + return self.format_period(char) + elif char == 'h': + if self.value.hour % 12 == 0: + return self.format(12, num) + else: + return self.format(self.value.hour % 12, num) + elif char == 'H': + return self.format(self.value.hour, num) + elif char == 'K': + return self.format(self.value.hour % 12, num) + elif char == 'k': + if self.value.hour == 0: + return self.format(24, num) + else: + return self.format(self.value.hour, num) + elif char == 'm': + return self.format(self.value.minute, num) + elif char == 's': + return self.format(self.value.second, num) + elif char == 'S': + return self.format_frac_seconds(num) + elif char == 'A': + return self.format_milliseconds_in_day(num) + elif char in ('z', 'Z', 'v', 'V'): + return self.format_timezone(char, num) + else: + raise KeyError('Unsupported date/time field %r' % char) + + def format_era(self, char, 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, char, num): + value = self.value.year + if char.isupper(): + week = self.get_week_number(self.get_day_of_year()) + if week == 0: + value -= 1 + year = self.format(value, num) + if num == 2: + year = year[-2:] + return year + + def format_quarter(self, char, num): + quarter = (self.value.month - 1) // 3 + 1 + if num <= 2: + return ('%%0%dd' % num) % quarter + width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] + context = {'Q': 'format', 'q': 'stand-alone'}[char] + return get_quarter_names(width, context, self.locale)[quarter] + + def format_month(self, char, num): + if num <= 2: + return ('%%0%dd' % num) % self.value.month + width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] + context = {'M': 'format', 'L': 'stand-alone'}[char] + return get_month_names(width, context, self.locale)[self.value.month] + + def format_week(self, char, num): + if char.islower(): # week of year + day_of_year = self.get_day_of_year() + week = self.get_week_number(day_of_year) + if week == 0: + date = self.value - timedelta(days=day_of_year) + week = self.get_week_number(self.get_day_of_year(date), + date.weekday()) + return self.format(week, num) + else: # week of month + week = self.get_week_number(self.value.day) + if week == 0: + date = self.value - timedelta(days=self.value.day) + week = self.get_week_number(date.day, date.weekday()) + pass + return '%d' % week + + def format_weekday(self, char, num): + if num < 3: + if char.islower(): + value = 7 - self.locale.first_week_day + self.value.weekday() + return self.format(value % 7 + 1, num) + num = 3 + weekday = self.value.weekday() + width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] + context = {3: 'format', 4: 'format', 5: 'stand-alone'}[num] + return get_day_names(width, context, self.locale)[weekday] + + def format_day_of_year(self, num): + return self.format(self.get_day_of_year(), num) + + def format_day_of_week_in_month(self): + return '%d' % ((self.value.day - 1) / 7 + 1) + + def format_period(self, char): + period = {0: 'am', 1: 'pm'}[int(self.value.hour >= 12)] + return get_period_names(locale=self.locale)[period] + + def format_frac_seconds(self, num): + value = str(self.value.microsecond) + return self.format(round(float('.%s' % value), num) * 10**num, num) + + def format_milliseconds_in_day(self, num): + msecs = self.value.microsecond // 1000 + self.value.second * 1000 + \ + self.value.minute * 60000 + self.value.hour * 3600000 + return self.format(msecs, num) + + def format_timezone(self, char, num): + width = {3: 'short', 4: 'long'}[max(3, num)] + if char == 'z': + return get_timezone_name(self.value, width, locale=self.locale) + elif char == 'Z': + return get_timezone_gmt(self.value, width, locale=self.locale) + elif char == 'v': + return get_timezone_name(self.value.tzinfo, width, + locale=self.locale) + elif char == 'V': + if num == 1: + return get_timezone_name(self.value.tzinfo, width, + uncommon=True, locale=self.locale) + return get_timezone_location(self.value.tzinfo, locale=self.locale) + + def format(self, value, length): + return ('%%0%dd' % length) % value + + def get_day_of_year(self, date=None): + if date is None: + date = self.value + return (date - date_(date.year, 1, 1)).days + 1 + + def get_week_number(self, day_of_period, day_of_week=None): + """Return the number of the week of a day within a period. This may be + the week number in a year or the week number in a month. + + Usually this will return a value equal to or greater than 1, but if the + first week of the period is so short that it actually counts as the last + week of the previous period, this function will return 0. + + >>> format = DateTimeFormat(date(2006, 1, 8), Locale.parse('de_DE')) + >>> format.get_week_number(6) + 1 + + >>> format = DateTimeFormat(date(2006, 1, 8), Locale.parse('en_US')) + >>> format.get_week_number(6) + 2 + + :param day_of_period: the number of the day in the period (usually + either the day of month or the day of year) + :param day_of_week: the week day; if ommitted, the week day of the + current date is assumed + """ + if day_of_week is None: + day_of_week = self.value.weekday() + first_day = (day_of_week - self.locale.first_week_day - + day_of_period + 1) % 7 + if first_day < 0: + first_day += 7 + week_number = (day_of_period + first_day - 1) / 7 + if 7 - first_day >= self.locale.min_week_days: + week_number += 1 + return week_number + + +PATTERN_CHARS = { + 'G': [1, 2, 3, 4, 5], # era + 'y': None, 'Y': None, 'u': None, # year + 'Q': [1, 2, 3, 4], 'q': [1, 2, 3, 4], # quarter + 'M': [1, 2, 3, 4, 5], 'L': [1, 2, 3, 4, 5], # month + 'w': [1, 2], 'W': [1], # week + 'd': [1, 2], 'D': [1, 2, 3], 'F': [1], 'g': None, # day + 'E': [1, 2, 3, 4, 5], 'e': [1, 2, 3, 4, 5], 'c': [1, 3, 4, 5], # week day + 'a': [1], # period + 'h': [1, 2], 'H': [1, 2], 'K': [1, 2], 'k': [1, 2], # hour + 'm': [1, 2], # minute + 's': [1, 2], 'S': None, 'A': None, # second + 'z': [1, 2, 3, 4], 'Z': [1, 2, 3, 4], 'v': [1, 4], 'V': [1, 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' + + Pattern can contain literal strings in single quotes: + + >>> parse_pattern("H:mm' Uhr 'z").format + u'%(H)s:%(mm)s Uhr %(z)s' + + An actual single quote can be used by using two adjacent single quote + characters: + + >>> parse_pattern("hh' o''clock'").format + u"%(hh)s o'clock" + + :param pattern: the formatting pattern to parse + """ + if type(pattern) is DateTimePattern: + 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 and fieldnum[0] not in 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.replace("''", '\0')): + 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 == "'": # end of quote + charbuf.extend(quotebuf) + quotebuf = None + else: # inside quote + quotebuf.append(char) + + if fieldchar[0]: + append_field() + elif charbuf: + append_chars() + + return DateTimePattern(pattern, u''.join(result).replace('\0', "'")) diff --git a/justice/external/babel/localedata.py b/justice/external/babel/localedata.py index 9308b5e..9e70f0b 100644 --- a/justice/external/babel/localedata.py +++ b/justice/external/babel/localedata.py @@ -1,209 +1,209 @@ -# -*- 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/. - -"""Low-level locale data access. - -:note: The `Locale` class, which uses this module under the hood, provides a - more convenient interface for accessing the locale data. -""" - -import os -import pickle -try: - import threading -except ImportError: - import dummy_threading as threading -from UserDict import DictMixin - -__all__ = ['exists', 'list', 'load'] -__docformat__ = 'restructuredtext en' - -_cache = {} -_cache_lock = threading.RLock() -_dirname = os.path.join(os.path.dirname(__file__), 'localedata') - - -def exists(name): - """Check whether locale data is available for the given locale. - - :param name: the locale identifier string - :return: `True` if the locale data exists, `False` otherwise - :rtype: `bool` - """ - if name in _cache: - return True - return os.path.exists(os.path.join(_dirname, '%s.dat' % name)) - - -def list(): - """Return a list of all locale identifiers for which locale data is - available. - - :return: a list of locale identifiers (strings) - :rtype: `list` - :since: version 0.8.1 - """ - return [stem for stem, extension in [ - os.path.splitext(filename) for filename in os.listdir(_dirname) - ] if extension == '.dat' and stem != 'root'] - - -def load(name, merge_inherited=True): - """Load the locale data for the given locale. - - The locale data is a dictionary that contains much of the data defined by - the Common Locale Data Repository (CLDR). This data is stored as a - collection of pickle files inside the ``babel`` package. - - >>> d = load('en_US') - >>> d['languages']['sv'] - u'Swedish' - - Note that the results are cached, and subsequent requests for the same - locale return the same dictionary: - - >>> d1 = load('en_US') - >>> d2 = load('en_US') - >>> d1 is d2 - True - - :param name: the locale identifier string (or "root") - :param merge_inherited: whether the inherited data should be merged into - the data of the requested locale - :return: the locale data - :rtype: `dict` - :raise `IOError`: if no locale data file is found for the given locale - identifer, or one of the locales it inherits from - """ - _cache_lock.acquire() - try: - data = _cache.get(name) - if not data: - # Load inherited data - if name == 'root' or not merge_inherited: - data = {} - else: - parts = name.split('_') - if len(parts) == 1: - parent = 'root' - else: - parent = '_'.join(parts[:-1]) - data = load(parent).copy() - filename = os.path.join(_dirname, '%s.dat' % name) - fileobj = open(filename, 'rb') - try: - if name != 'root' and merge_inherited: - merge(data, pickle.load(fileobj)) - else: - data = pickle.load(fileobj) - _cache[name] = data - finally: - fileobj.close() - return data - finally: - _cache_lock.release() - - -def merge(dict1, dict2): - """Merge the data from `dict2` into the `dict1` dictionary, making copies - of nested dictionaries. - - >>> d = {1: 'foo', 3: 'baz'} - >>> merge(d, {1: 'Foo', 2: 'Bar'}) - >>> items = d.items(); items.sort(); items - [(1, 'Foo'), (2, 'Bar'), (3, 'baz')] - - :param dict1: the dictionary to merge into - :param dict2: the dictionary containing the data that should be merged - """ - for key, val2 in dict2.items(): - if val2 is not None: - val1 = dict1.get(key) - if isinstance(val2, dict): - if val1 is None: - val1 = {} - if isinstance(val1, Alias): - val1 = (val1, val2) - elif isinstance(val1, tuple): - alias, others = val1 - others = others.copy() - merge(others, val2) - val1 = (alias, others) - else: - val1 = val1.copy() - merge(val1, val2) - else: - val1 = val2 - dict1[key] = val1 - - -class Alias(object): - """Representation of an alias in the locale data. - - An alias is a value that refers to some other part of the locale data, - as specified by the `keys`. - """ - - def __init__(self, keys): - self.keys = tuple(keys) - - def __repr__(self): - return '<%s %r>' % (type(self).__name__, self.keys) - - def resolve(self, data): - """Resolve the alias based on the given data. - - This is done recursively, so if one alias resolves to a second alias, - that second alias will also be resolved. - - :param data: the locale data - :type data: `dict` - """ - base = data - for key in self.keys: - data = data[key] - if isinstance(data, Alias): - data = data.resolve(base) - elif isinstance(data, tuple): - alias, others = data - data = alias.resolve(base) - return data - - -class LocaleDataDict(DictMixin, dict): - """Dictionary wrapper that automatically resolves aliases to the actual - values. - """ - - def __init__(self, data, base=None): - dict.__init__(self, data) - if base is None: - base = data - self.base = base - - def __getitem__(self, key): - orig = val = dict.__getitem__(self, key) - if isinstance(val, Alias): # resolve an alias - val = val.resolve(self.base) - if isinstance(val, tuple): # Merge a partial dict with an alias - alias, others = val - val = alias.resolve(self.base).copy() - merge(val, others) - if type(val) is dict: # Return a nested alias-resolving dict - val = LocaleDataDict(val, base=self.base) - if val is not orig: - self[key] = val - return val - - def copy(self): - return LocaleDataDict(dict.copy(self), base=self.base) +# -*- 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/. + +"""Low-level locale data access. + +:note: The `Locale` class, which uses this module under the hood, provides a + more convenient interface for accessing the locale data. +""" + +import os +import pickle +try: + import threading +except ImportError: + import dummy_threading as threading +from UserDict import DictMixin + +__all__ = ['exists', 'list', 'load'] +__docformat__ = 'restructuredtext en' + +_cache = {} +_cache_lock = threading.RLock() +_dirname = os.path.join(os.path.dirname(__file__), 'localedata') + + +def exists(name): + """Check whether locale data is available for the given locale. + + :param name: the locale identifier string + :return: `True` if the locale data exists, `False` otherwise + :rtype: `bool` + """ + if name in _cache: + return True + return os.path.exists(os.path.join(_dirname, '%s.dat' % name)) + + +def list(): + """Return a list of all locale identifiers for which locale data is + available. + + :return: a list of locale identifiers (strings) + :rtype: `list` + :since: version 0.8.1 + """ + return [stem for stem, extension in [ + os.path.splitext(filename) for filename in os.listdir(_dirname) + ] if extension == '.dat' and stem != 'root'] + + +def load(name, merge_inherited=True): + """Load the locale data for the given locale. + + The locale data is a dictionary that contains much of the data defined by + the Common Locale Data Repository (CLDR). This data is stored as a + collection of pickle files inside the ``babel`` package. + + >>> d = load('en_US') + >>> d['languages']['sv'] + u'Swedish' + + Note that the results are cached, and subsequent requests for the same + locale return the same dictionary: + + >>> d1 = load('en_US') + >>> d2 = load('en_US') + >>> d1 is d2 + True + + :param name: the locale identifier string (or "root") + :param merge_inherited: whether the inherited data should be merged into + the data of the requested locale + :return: the locale data + :rtype: `dict` + :raise `IOError`: if no locale data file is found for the given locale + identifer, or one of the locales it inherits from + """ + _cache_lock.acquire() + try: + data = _cache.get(name) + if not data: + # Load inherited data + if name == 'root' or not merge_inherited: + data = {} + else: + parts = name.split('_') + if len(parts) == 1: + parent = 'root' + else: + parent = '_'.join(parts[:-1]) + data = load(parent).copy() + filename = os.path.join(_dirname, '%s.dat' % name) + fileobj = open(filename, 'rb') + try: + if name != 'root' and merge_inherited: + merge(data, pickle.load(fileobj)) + else: + data = pickle.load(fileobj) + _cache[name] = data + finally: + fileobj.close() + return data + finally: + _cache_lock.release() + + +def merge(dict1, dict2): + """Merge the data from `dict2` into the `dict1` dictionary, making copies + of nested dictionaries. + + >>> d = {1: 'foo', 3: 'baz'} + >>> merge(d, {1: 'Foo', 2: 'Bar'}) + >>> items = d.items(); items.sort(); items + [(1, 'Foo'), (2, 'Bar'), (3, 'baz')] + + :param dict1: the dictionary to merge into + :param dict2: the dictionary containing the data that should be merged + """ + for key, val2 in dict2.items(): + if val2 is not None: + val1 = dict1.get(key) + if isinstance(val2, dict): + if val1 is None: + val1 = {} + if isinstance(val1, Alias): + val1 = (val1, val2) + elif isinstance(val1, tuple): + alias, others = val1 + others = others.copy() + merge(others, val2) + val1 = (alias, others) + else: + val1 = val1.copy() + merge(val1, val2) + else: + val1 = val2 + dict1[key] = val1 + + +class Alias(object): + """Representation of an alias in the locale data. + + An alias is a value that refers to some other part of the locale data, + as specified by the `keys`. + """ + + def __init__(self, keys): + self.keys = tuple(keys) + + def __repr__(self): + return '<%s %r>' % (type(self).__name__, self.keys) + + def resolve(self, data): + """Resolve the alias based on the given data. + + This is done recursively, so if one alias resolves to a second alias, + that second alias will also be resolved. + + :param data: the locale data + :type data: `dict` + """ + base = data + for key in self.keys: + data = data[key] + if isinstance(data, Alias): + data = data.resolve(base) + elif isinstance(data, tuple): + alias, others = data + data = alias.resolve(base) + return data + + +class LocaleDataDict(DictMixin, dict): + """Dictionary wrapper that automatically resolves aliases to the actual + values. + """ + + def __init__(self, data, base=None): + dict.__init__(self, data) + if base is None: + base = data + self.base = base + + def __getitem__(self, key): + orig = val = dict.__getitem__(self, key) + if isinstance(val, Alias): # resolve an alias + val = val.resolve(self.base) + if isinstance(val, tuple): # Merge a partial dict with an alias + alias, others = val + val = alias.resolve(self.base).copy() + merge(val, others) + if type(val) is dict: # Return a nested alias-resolving dict + val = LocaleDataDict(val, base=self.base) + if val is not orig: + self[key] = val + return val + + def copy(self): + return LocaleDataDict(dict.copy(self), base=self.base) diff --git a/justice/external/babel/messages/__init__.py b/justice/external/babel/messages/__init__.py index 283e1f8..da2e38d 100644 --- a/justice/external/babel/messages/__init__.py +++ b/justice/external/babel/messages/__init__.py @@ -1,16 +1,16 @@ -# -*- 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.""" - -from babel.messages.catalog import * +# -*- 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.""" + +from babel.messages.catalog import * diff --git a/justice/external/babel/messages/catalog.py b/justice/external/babel/messages/catalog.py index 45b3be7..ca50d8b 100644 --- a/justice/external/babel/messages/catalog.py +++ b/justice/external/babel/messages/catalog.py @@ -1,768 +1,768 @@ -# -*- 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/. - -"""Data structures for message catalogs.""" - -from cgi import parse_header -from datetime import datetime -from difflib import get_close_matches -from email import message_from_string -from copy import copy -import re -import time - -from babel import __version__ as VERSION -from babel.core import Locale -from babel.dates import format_datetime -from babel.messages.plurals import get_plural -from babel.util import odict, distinct, set, LOCALTZ, UTC, FixedOffsetTimezone - -__all__ = ['Message', 'Catalog', 'TranslationError'] -__docformat__ = 'restructuredtext en' - - -PYTHON_FORMAT = re.compile(r'''(?x) - \% - (?:\(([\w]*)\))? - ( - [-#0\ +]?(?:\*|[\d]+)? - (?:\.(?:\*|[\d]+))? - [hlL]? - ) - ([diouxXeEfFgGcrs%]) -''') - - -class Message(object): - """Representation of a single message in a catalog.""" - - def __init__(self, id, string=u'', locations=(), flags=(), auto_comments=(), - user_comments=(), previous_id=(), lineno=None): - """Create the message object. - - :param id: the message ID, or a ``(singular, plural)`` tuple for - pluralizable messages - :param string: the translated message string, or a - ``(singular, plural)`` tuple for pluralizable messages - :param locations: a sequence of ``(filenname, lineno)`` tuples - :param flags: a set or sequence of flags - :param auto_comments: a sequence of automatic comments for the message - :param user_comments: a sequence of user comments for the message - :param previous_id: the previous message ID, or a ``(singular, plural)`` - tuple for pluralizable messages - :param lineno: the line number on which the msgid line was found in the - PO file, if any - """ - self.id = id #: The message ID - if not string and self.pluralizable: - string = (u'', u'') - self.string = string #: The message translation - self.locations = list(distinct(locations)) - self.flags = set(flags) - if id and self.python_format: - self.flags.add('python-format') - else: - self.flags.discard('python-format') - self.auto_comments = list(distinct(auto_comments)) - self.user_comments = list(distinct(user_comments)) - if isinstance(previous_id, basestring): - self.previous_id = [previous_id] - else: - self.previous_id = list(previous_id) - self.lineno = lineno - - def __repr__(self): - return '<%s %r (flags: %r)>' % (type(self).__name__, self.id, - list(self.flags)) - - def __cmp__(self, obj): - """Compare Messages, taking into account plural ids""" - if isinstance(obj, Message): - plural = self.pluralizable - obj_plural = obj.pluralizable - if plural and obj_plural: - return cmp(self.id[0], obj.id[0]) - elif plural: - return cmp(self.id[0], obj.id) - elif obj_plural: - return cmp(self.id, obj.id[0]) - return cmp(self.id, obj.id) - - def clone(self): - return Message(*map(copy, (self.id, self.string, self.locations, - self.flags, self.auto_comments, - self.user_comments, self.previous_id, - self.lineno))) - - def check(self, catalog=None): - """Run various validation checks on the message. Some validations - are only performed if the catalog is provided. This method returns - a sequence of `TranslationError` objects. - - :rtype: ``iterator`` - :param catalog: A catalog instance that is passed to the checkers - :see: `Catalog.check` for a way to perform checks for all messages - in a catalog. - """ - from babel.messages.checkers import checkers - errors = [] - for checker in checkers: - try: - checker(catalog, self) - except TranslationError, e: - errors.append(e) - return errors - - def fuzzy(self): - return 'fuzzy' in self.flags - fuzzy = property(fuzzy, doc="""\ - Whether the translation is fuzzy. - - >>> Message('foo').fuzzy - False - >>> msg = Message('foo', 'foo', flags=['fuzzy']) - >>> msg.fuzzy - True - >>> msg - - - :type: `bool` - """) - - def pluralizable(self): - return isinstance(self.id, (list, tuple)) - pluralizable = property(pluralizable, doc="""\ - Whether the message is plurizable. - - >>> Message('foo').pluralizable - False - >>> Message(('foo', 'bar')).pluralizable - True - - :type: `bool` - """) - - def python_format(self): - ids = self.id - if not isinstance(ids, (list, tuple)): - ids = [ids] - return bool(filter(None, [PYTHON_FORMAT.search(id) for id in ids])) - python_format = property(python_format, doc="""\ - Whether the message contains Python-style parameters. - - >>> Message('foo %(name)s bar').python_format - True - >>> Message(('foo %(name)s', 'foo %(name)s')).python_format - True - - :type: `bool` - """) - - -class TranslationError(Exception): - """Exception thrown by translation checkers when invalid message - translations are encountered.""" - - -DEFAULT_HEADER = u"""\ -# Translations template for PROJECT. -# Copyright (C) YEAR ORGANIZATION -# This file is distributed under the same license as the PROJECT project. -# FIRST AUTHOR , YEAR. -#""" - - -class Catalog(object): - """Representation of a message catalog.""" - - def __init__(self, locale=None, domain=None, header_comment=DEFAULT_HEADER, - project=None, version=None, copyright_holder=None, - msgid_bugs_address=None, creation_date=None, - revision_date=None, last_translator=None, language_team=None, - charset='utf-8', fuzzy=True): - """Initialize the catalog object. - - :param locale: the locale identifier or `Locale` object, or `None` - if the catalog is not bound to a locale (which basically - means it's a template) - :param domain: the message domain - :param header_comment: the header comment as string, or `None` for the - default header - :param project: the project's name - :param version: the project's version - :param copyright_holder: the copyright holder of the catalog - :param msgid_bugs_address: the email address or URL to submit bug - reports to - :param creation_date: the date the catalog was created - :param revision_date: the date the catalog was revised - :param last_translator: the name and email of the last translator - :param language_team: the name and email of the language team - :param charset: the encoding to use in the output - :param fuzzy: the fuzzy bit on the catalog header - """ - self.domain = domain #: The message domain - if locale: - locale = Locale.parse(locale) - self.locale = locale #: The locale or `None` - self._header_comment = header_comment - self._messages = odict() - - self.project = project or 'PROJECT' #: The project name - self.version = version or 'VERSION' #: The project version - self.copyright_holder = copyright_holder or 'ORGANIZATION' - self.msgid_bugs_address = msgid_bugs_address or 'EMAIL@ADDRESS' - - self.last_translator = last_translator or 'FULL NAME ' - """Name and email address of the last translator.""" - self.language_team = language_team or 'LANGUAGE ' - """Name and email address of the language team.""" - - self.charset = charset or 'utf-8' - - if creation_date is None: - creation_date = datetime.now(LOCALTZ) - elif isinstance(creation_date, datetime) and not creation_date.tzinfo: - creation_date = creation_date.replace(tzinfo=LOCALTZ) - self.creation_date = creation_date #: Creation date of the template - if revision_date is None: - revision_date = datetime.now(LOCALTZ) - elif isinstance(revision_date, datetime) and not revision_date.tzinfo: - revision_date = revision_date.replace(tzinfo=LOCALTZ) - self.revision_date = revision_date #: Last revision date of the catalog - self.fuzzy = fuzzy #: Catalog header fuzzy bit (`True` or `False`) - - self.obsolete = odict() #: Dictionary of obsolete messages - self._num_plurals = None - self._plural_expr = None - - def _get_header_comment(self): - comment = self._header_comment - comment = comment.replace('PROJECT', self.project) \ - .replace('VERSION', self.version) \ - .replace('YEAR', self.revision_date.strftime('%Y')) \ - .replace('ORGANIZATION', self.copyright_holder) - if self.locale: - comment = comment.replace('Translations template', '%s translations' - % self.locale.english_name) - return comment - - def _set_header_comment(self, string): - self._header_comment = string - - header_comment = property(_get_header_comment, _set_header_comment, doc="""\ - The header comment for the catalog. - - >>> catalog = Catalog(project='Foobar', version='1.0', - ... copyright_holder='Foo Company') - >>> print catalog.header_comment #doctest: +ELLIPSIS - # Translations template for Foobar. - # Copyright (C) ... Foo Company - # This file is distributed under the same license as the Foobar project. - # FIRST AUTHOR , .... - # - - The header can also be set from a string. Any known upper-case variables - will be replaced when the header is retrieved again: - - >>> catalog = Catalog(project='Foobar', version='1.0', - ... copyright_holder='Foo Company') - >>> catalog.header_comment = '''\\ - ... # The POT for my really cool PROJECT project. - ... # Copyright (C) 1990-2003 ORGANIZATION - ... # This file is distributed under the same license as the PROJECT - ... # project. - ... #''' - >>> print catalog.header_comment - # The POT for my really cool Foobar project. - # Copyright (C) 1990-2003 Foo Company - # This file is distributed under the same license as the Foobar - # project. - # - - :type: `unicode` - """) - - def _get_mime_headers(self): - headers = [] - headers.append(('Project-Id-Version', - '%s %s' % (self.project, self.version))) - headers.append(('Report-Msgid-Bugs-To', self.msgid_bugs_address)) - headers.append(('POT-Creation-Date', - format_datetime(self.creation_date, 'yyyy-MM-dd HH:mmZ', - locale='en'))) - if self.locale is None: - headers.append(('PO-Revision-Date', 'YEAR-MO-DA HO:MI+ZONE')) - headers.append(('Last-Translator', 'FULL NAME ')) - headers.append(('Language-Team', 'LANGUAGE ')) - else: - headers.append(('PO-Revision-Date', - format_datetime(self.revision_date, - 'yyyy-MM-dd HH:mmZ', locale='en'))) - headers.append(('Last-Translator', self.last_translator)) - headers.append(('Language-Team', - self.language_team.replace('LANGUAGE', - str(self.locale)))) - headers.append(('Plural-Forms', self.plural_forms)) - headers.append(('MIME-Version', '1.0')) - headers.append(('Content-Type', - 'text/plain; charset=%s' % self.charset)) - headers.append(('Content-Transfer-Encoding', '8bit')) - headers.append(('Generated-By', 'Babel %s\n' % VERSION)) - return headers - - def _set_mime_headers(self, headers): - for name, value in headers: - if name.lower() == 'content-type': - mimetype, params = parse_header(value) - if 'charset' in params: - self.charset = params['charset'].lower() - break - for name, value in headers: - name = name.lower().decode(self.charset) - value = value.decode(self.charset) - if name == 'project-id-version': - parts = value.split(' ') - self.project = u' '.join(parts[:-1]) - self.version = parts[-1] - elif name == 'report-msgid-bugs-to': - self.msgid_bugs_address = value - elif name == 'last-translator': - self.last_translator = value - elif name == 'language-team': - self.language_team = value - elif name == 'plural-forms': - _, params = parse_header(' ;' + value) - self._num_plurals = int(params.get('nplurals', 2)) - self._plural_expr = params.get('plural', '(n != 1)') - elif name == 'pot-creation-date': - # FIXME: this should use dates.parse_datetime as soon as that - # is ready - value, tzoffset, _ = re.split('([+-]\d{4})$', value, 1) - - tt = time.strptime(value, '%Y-%m-%d %H:%M') - ts = time.mktime(tt) - - # Separate the offset into a sign component, hours, and minutes - plus_minus_s, rest = tzoffset[0], tzoffset[1:] - hours_offset_s, mins_offset_s = rest[:2], rest[2:] - - # Make them all integers - plus_minus = int(plus_minus_s + '1') - hours_offset = int(hours_offset_s) - mins_offset = int(mins_offset_s) - - # Calculate net offset - net_mins_offset = hours_offset * 60 - net_mins_offset += mins_offset - net_mins_offset *= plus_minus - - # Create an offset object - tzoffset = FixedOffsetTimezone(net_mins_offset) - - # Store the offset in a datetime object - dt = datetime.fromtimestamp(ts) - self.creation_date = dt.replace(tzinfo=tzoffset) - elif name == 'po-revision-date': - # Keep the value if it's not the default one - if 'YEAR' not in value: - # FIXME: this should use dates.parse_datetime as soon as - # that is ready - value, tzoffset, _ = re.split('([+-]\d{4})$', value, 1) - tt = time.strptime(value, '%Y-%m-%d %H:%M') - ts = time.mktime(tt) - - # Separate the offset into a sign component, hours, and - # minutes - plus_minus_s, rest = tzoffset[0], tzoffset[1:] - hours_offset_s, mins_offset_s = rest[:2], rest[2:] - - # Make them all integers - plus_minus = int(plus_minus_s + '1') - hours_offset = int(hours_offset_s) - mins_offset = int(mins_offset_s) - - # Calculate net offset - net_mins_offset = hours_offset * 60 - net_mins_offset += mins_offset - net_mins_offset *= plus_minus - - # Create an offset object - tzoffset = FixedOffsetTimezone(net_mins_offset) - - # Store the offset in a datetime object - dt = datetime.fromtimestamp(ts) - self.revision_date = dt.replace(tzinfo=tzoffset) - - mime_headers = property(_get_mime_headers, _set_mime_headers, doc="""\ - The MIME headers of the catalog, used for the special ``msgid ""`` entry. - - The behavior of this property changes slightly depending on whether a locale - is set or not, the latter indicating that the catalog is actually a template - for actual translations. - - Here's an example of the output for such a catalog template: - - >>> created = datetime(1990, 4, 1, 15, 30, tzinfo=UTC) - >>> catalog = Catalog(project='Foobar', version='1.0', - ... creation_date=created) - >>> for name, value in catalog.mime_headers: - ... print '%s: %s' % (name, value) - Project-Id-Version: Foobar 1.0 - Report-Msgid-Bugs-To: EMAIL@ADDRESS - POT-Creation-Date: 1990-04-01 15:30+0000 - PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE - Last-Translator: FULL NAME - Language-Team: LANGUAGE - MIME-Version: 1.0 - Content-Type: text/plain; charset=utf-8 - Content-Transfer-Encoding: 8bit - Generated-By: Babel ... - - And here's an example of the output when the locale is set: - - >>> revised = datetime(1990, 8, 3, 12, 0, tzinfo=UTC) - >>> catalog = Catalog(locale='de_DE', project='Foobar', version='1.0', - ... creation_date=created, revision_date=revised, - ... last_translator='John Doe ', - ... language_team='de_DE ') - >>> for name, value in catalog.mime_headers: - ... print '%s: %s' % (name, value) - Project-Id-Version: Foobar 1.0 - Report-Msgid-Bugs-To: EMAIL@ADDRESS - POT-Creation-Date: 1990-04-01 15:30+0000 - PO-Revision-Date: 1990-08-03 12:00+0000 - Last-Translator: John Doe - Language-Team: de_DE - Plural-Forms: nplurals=2; plural=(n != 1) - MIME-Version: 1.0 - Content-Type: text/plain; charset=utf-8 - Content-Transfer-Encoding: 8bit - Generated-By: Babel ... - - :type: `list` - """) - - def num_plurals(self): - if self._num_plurals is None: - num = 2 - if self.locale: - num = get_plural(self.locale)[0] - self._num_plurals = num - return self._num_plurals - num_plurals = property(num_plurals, doc="""\ - The number of plurals used by the catalog or locale. - - >>> Catalog(locale='en').num_plurals - 2 - >>> Catalog(locale='ga').num_plurals - 3 - - :type: `int` - """) - - def plural_expr(self): - if self._plural_expr is None: - expr = '(n != 1)' - if self.locale: - expr = get_plural(self.locale)[1] - self._plural_expr = expr - return self._plural_expr - plural_expr = property(plural_expr, doc="""\ - The plural expression used by the catalog or locale. - - >>> Catalog(locale='en').plural_expr - '(n != 1)' - >>> Catalog(locale='ga').plural_expr - '(n==1 ? 0 : n==2 ? 1 : 2)' - - :type: `basestring` - """) - - def plural_forms(self): - return 'nplurals=%s; plural=%s' % (self.num_plurals, self.plural_expr) - plural_forms = property(plural_forms, doc="""\ - Return the plural forms declaration for the locale. - - >>> Catalog(locale='en').plural_forms - 'nplurals=2; plural=(n != 1)' - >>> Catalog(locale='pt_BR').plural_forms - 'nplurals=2; plural=(n > 1)' - - :type: `str` - """) - - def __contains__(self, id): - """Return whether the catalog has a message with the specified ID.""" - return self._key_for(id) in self._messages - - def __len__(self): - """The number of messages in the catalog. - - This does not include the special ``msgid ""`` entry. - """ - return len(self._messages) - - def __iter__(self): - """Iterates through all the entries in the catalog, in the order they - were added, yielding a `Message` object for every entry. - - :rtype: ``iterator`` - """ - buf = [] - for name, value in self.mime_headers: - buf.append('%s: %s' % (name, value)) - flags = set() - if self.fuzzy: - flags |= set(['fuzzy']) - yield Message(u'', '\n'.join(buf), flags=flags) - for key in self._messages: - yield self._messages[key] - - def __repr__(self): - locale = '' - if self.locale: - locale = ' %s' % self.locale - return '<%s %r%s>' % (type(self).__name__, self.domain, locale) - - def __delitem__(self, id): - """Delete the message with the specified ID.""" - key = self._key_for(id) - if key in self._messages: - del self._messages[key] - - def __getitem__(self, id): - """Return the message with the specified ID. - - :param id: the message ID - :return: the message with the specified ID, or `None` if no such message - is in the catalog - :rtype: `Message` - """ - return self._messages.get(self._key_for(id)) - - def __setitem__(self, id, message): - """Add or update the message with the specified ID. - - >>> catalog = Catalog() - >>> catalog[u'foo'] = Message(u'foo') - >>> catalog[u'foo'] - - - If a message with that ID is already in the catalog, it is updated - to include the locations and flags of the new message. - - >>> catalog = Catalog() - >>> catalog[u'foo'] = Message(u'foo', locations=[('main.py', 1)]) - >>> catalog[u'foo'].locations - [('main.py', 1)] - >>> catalog[u'foo'] = Message(u'foo', locations=[('utils.py', 5)]) - >>> catalog[u'foo'].locations - [('main.py', 1), ('utils.py', 5)] - - :param id: the message ID - :param message: the `Message` object - """ - assert isinstance(message, Message), 'expected a Message object' - key = self._key_for(id) - current = self._messages.get(key) - if current: - if message.pluralizable and not current.pluralizable: - # The new message adds pluralization - current.id = message.id - current.string = message.string - current.locations = list(distinct(current.locations + - message.locations)) - current.auto_comments = list(distinct(current.auto_comments + - message.auto_comments)) - current.user_comments = list(distinct(current.user_comments + - message.user_comments)) - current.flags |= message.flags - message = current - elif id == '': - # special treatment for the header message - headers = message_from_string(message.string.encode(self.charset)) - self.mime_headers = headers.items() - self.header_comment = '\n'.join(['# %s' % comment for comment - in message.user_comments]) - self.fuzzy = message.fuzzy - else: - if isinstance(id, (list, tuple)): - assert isinstance(message.string, (list, tuple)), \ - 'Expected sequence but got %s' % type(message.string) - self._messages[key] = message - - def add(self, id, string=None, locations=(), flags=(), auto_comments=(), - user_comments=(), previous_id=(), lineno=None): - """Add or update the message with the specified ID. - - >>> catalog = Catalog() - >>> catalog.add(u'foo') - >>> catalog[u'foo'] - - - This method simply constructs a `Message` object with the given - arguments and invokes `__setitem__` with that object. - - :param id: the message ID, or a ``(singular, plural)`` tuple for - pluralizable messages - :param string: the translated message string, or a - ``(singular, plural)`` tuple for pluralizable messages - :param locations: a sequence of ``(filenname, lineno)`` tuples - :param flags: a set or sequence of flags - :param auto_comments: a sequence of automatic comments - :param user_comments: a sequence of user comments - :param previous_id: the previous message ID, or a ``(singular, plural)`` - tuple for pluralizable messages - :param lineno: the line number on which the msgid line was found in the - PO file, if any - """ - self[id] = Message(id, string, list(locations), flags, auto_comments, - user_comments, previous_id, lineno=lineno) - - def check(self): - """Run various validation checks on the translations in the catalog. - - For every message which fails validation, this method yield a - ``(message, errors)`` tuple, where ``message`` is the `Message` object - and ``errors`` is a sequence of `TranslationError` objects. - - :rtype: ``iterator`` - """ - for message in self._messages.values(): - errors = message.check(catalog=self) - if errors: - yield message, errors - - def update(self, template, no_fuzzy_matching=False): - """Update the catalog based on the given template catalog. - - >>> from babel.messages import Catalog - >>> template = Catalog() - >>> template.add('green', locations=[('main.py', 99)]) - >>> template.add('blue', locations=[('main.py', 100)]) - >>> template.add(('salad', 'salads'), locations=[('util.py', 42)]) - >>> catalog = Catalog(locale='de_DE') - >>> catalog.add('blue', u'blau', locations=[('main.py', 98)]) - >>> catalog.add('head', u'Kopf', locations=[('util.py', 33)]) - >>> catalog.add(('salad', 'salads'), (u'Salat', u'Salate'), - ... locations=[('util.py', 38)]) - - >>> catalog.update(template) - >>> len(catalog) - 3 - - >>> msg1 = catalog['green'] - >>> msg1.string - >>> msg1.locations - [('main.py', 99)] - - >>> msg2 = catalog['blue'] - >>> msg2.string - u'blau' - >>> msg2.locations - [('main.py', 100)] - - >>> msg3 = catalog['salad'] - >>> msg3.string - (u'Salat', u'Salate') - >>> msg3.locations - [('util.py', 42)] - - Messages that are in the catalog but not in the template are removed - from the main collection, but can still be accessed via the `obsolete` - member: - - >>> 'head' in catalog - False - >>> catalog.obsolete.values() - [] - - :param template: the reference catalog, usually read from a POT file - :param no_fuzzy_matching: whether to use fuzzy matching of message IDs - """ - messages = self._messages - remaining = messages.copy() - self._messages = odict() - - # Prepare for fuzzy matching - fuzzy_candidates = [] - if not no_fuzzy_matching: - fuzzy_candidates = [ - self._key_for(msgid) for msgid in messages - if msgid and messages[msgid].string - ] - fuzzy_matches = set() - - def _merge(message, oldkey, newkey): - message = message.clone() - fuzzy = False - if oldkey != newkey: - fuzzy = True - fuzzy_matches.add(oldkey) - oldmsg = messages.get(oldkey) - if isinstance(oldmsg.id, basestring): - message.previous_id = [oldmsg.id] - else: - message.previous_id = list(oldmsg.id) - else: - oldmsg = remaining.pop(oldkey, None) - message.string = oldmsg.string - if isinstance(message.id, (list, tuple)): - if not isinstance(message.string, (list, tuple)): - fuzzy = True - message.string = tuple( - [message.string] + ([u''] * (len(message.id) - 1)) - ) - elif len(message.string) != self.num_plurals: - fuzzy = True - message.string = tuple(message.string[:len(oldmsg.string)]) - elif isinstance(message.string, (list, tuple)): - fuzzy = True - message.string = message.string[0] - message.flags |= oldmsg.flags - if fuzzy: - message.flags |= set([u'fuzzy']) - self[message.id] = message - - for message in template: - if message.id: - key = self._key_for(message.id) - if key in messages: - _merge(message, key, key) - else: - if no_fuzzy_matching is False: - # do some fuzzy matching with difflib - matches = get_close_matches(key.lower().strip(), - fuzzy_candidates, 1) - if matches: - _merge(message, matches[0], key) - continue - - self[message.id] = message - - self.obsolete = odict() - for msgid in remaining: - if no_fuzzy_matching or msgid not in fuzzy_matches: - self.obsolete[msgid] = remaining[msgid] - # Make updated catalog's POT-Creation-Date equal to the template - # used to update the catalog - self.creation_date = template.creation_date - - def _key_for(self, id): - """The key for a message is just the singular ID even for pluralizable - messages. - """ - key = id - if isinstance(key, (list, tuple)): - key = id[0] - return key +# -*- 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/. + +"""Data structures for message catalogs.""" + +from cgi import parse_header +from datetime import datetime +from difflib import get_close_matches +from email import message_from_string +from copy import copy +import re +import time + +from babel import __version__ as VERSION +from babel.core import Locale +from babel.dates import format_datetime +from babel.messages.plurals import get_plural +from babel.util import odict, distinct, set, LOCALTZ, UTC, FixedOffsetTimezone + +__all__ = ['Message', 'Catalog', 'TranslationError'] +__docformat__ = 'restructuredtext en' + + +PYTHON_FORMAT = re.compile(r'''(?x) + \% + (?:\(([\w]*)\))? + ( + [-#0\ +]?(?:\*|[\d]+)? + (?:\.(?:\*|[\d]+))? + [hlL]? + ) + ([diouxXeEfFgGcrs%]) +''') + + +class Message(object): + """Representation of a single message in a catalog.""" + + def __init__(self, id, string=u'', locations=(), flags=(), auto_comments=(), + user_comments=(), previous_id=(), lineno=None): + """Create the message object. + + :param id: the message ID, or a ``(singular, plural)`` tuple for + pluralizable messages + :param string: the translated message string, or a + ``(singular, plural)`` tuple for pluralizable messages + :param locations: a sequence of ``(filenname, lineno)`` tuples + :param flags: a set or sequence of flags + :param auto_comments: a sequence of automatic comments for the message + :param user_comments: a sequence of user comments for the message + :param previous_id: the previous message ID, or a ``(singular, plural)`` + tuple for pluralizable messages + :param lineno: the line number on which the msgid line was found in the + PO file, if any + """ + self.id = id #: The message ID + if not string and self.pluralizable: + string = (u'', u'') + self.string = string #: The message translation + self.locations = list(distinct(locations)) + self.flags = set(flags) + if id and self.python_format: + self.flags.add('python-format') + else: + self.flags.discard('python-format') + self.auto_comments = list(distinct(auto_comments)) + self.user_comments = list(distinct(user_comments)) + if isinstance(previous_id, basestring): + self.previous_id = [previous_id] + else: + self.previous_id = list(previous_id) + self.lineno = lineno + + def __repr__(self): + return '<%s %r (flags: %r)>' % (type(self).__name__, self.id, + list(self.flags)) + + def __cmp__(self, obj): + """Compare Messages, taking into account plural ids""" + if isinstance(obj, Message): + plural = self.pluralizable + obj_plural = obj.pluralizable + if plural and obj_plural: + return cmp(self.id[0], obj.id[0]) + elif plural: + return cmp(self.id[0], obj.id) + elif obj_plural: + return cmp(self.id, obj.id[0]) + return cmp(self.id, obj.id) + + def clone(self): + return Message(*map(copy, (self.id, self.string, self.locations, + self.flags, self.auto_comments, + self.user_comments, self.previous_id, + self.lineno))) + + def check(self, catalog=None): + """Run various validation checks on the message. Some validations + are only performed if the catalog is provided. This method returns + a sequence of `TranslationError` objects. + + :rtype: ``iterator`` + :param catalog: A catalog instance that is passed to the checkers + :see: `Catalog.check` for a way to perform checks for all messages + in a catalog. + """ + from babel.messages.checkers import checkers + errors = [] + for checker in checkers: + try: + checker(catalog, self) + except TranslationError, e: + errors.append(e) + return errors + + def fuzzy(self): + return 'fuzzy' in self.flags + fuzzy = property(fuzzy, doc="""\ + Whether the translation is fuzzy. + + >>> Message('foo').fuzzy + False + >>> msg = Message('foo', 'foo', flags=['fuzzy']) + >>> msg.fuzzy + True + >>> msg + + + :type: `bool` + """) + + def pluralizable(self): + return isinstance(self.id, (list, tuple)) + pluralizable = property(pluralizable, doc="""\ + Whether the message is plurizable. + + >>> Message('foo').pluralizable + False + >>> Message(('foo', 'bar')).pluralizable + True + + :type: `bool` + """) + + def python_format(self): + ids = self.id + if not isinstance(ids, (list, tuple)): + ids = [ids] + return bool(filter(None, [PYTHON_FORMAT.search(id) for id in ids])) + python_format = property(python_format, doc="""\ + Whether the message contains Python-style parameters. + + >>> Message('foo %(name)s bar').python_format + True + >>> Message(('foo %(name)s', 'foo %(name)s')).python_format + True + + :type: `bool` + """) + + +class TranslationError(Exception): + """Exception thrown by translation checkers when invalid message + translations are encountered.""" + + +DEFAULT_HEADER = u"""\ +# Translations template for PROJECT. +# Copyright (C) YEAR ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , YEAR. +#""" + + +class Catalog(object): + """Representation of a message catalog.""" + + def __init__(self, locale=None, domain=None, header_comment=DEFAULT_HEADER, + project=None, version=None, copyright_holder=None, + msgid_bugs_address=None, creation_date=None, + revision_date=None, last_translator=None, language_team=None, + charset='utf-8', fuzzy=True): + """Initialize the catalog object. + + :param locale: the locale identifier or `Locale` object, or `None` + if the catalog is not bound to a locale (which basically + means it's a template) + :param domain: the message domain + :param header_comment: the header comment as string, or `None` for the + default header + :param project: the project's name + :param version: the project's version + :param copyright_holder: the copyright holder of the catalog + :param msgid_bugs_address: the email address or URL to submit bug + reports to + :param creation_date: the date the catalog was created + :param revision_date: the date the catalog was revised + :param last_translator: the name and email of the last translator + :param language_team: the name and email of the language team + :param charset: the encoding to use in the output + :param fuzzy: the fuzzy bit on the catalog header + """ + self.domain = domain #: The message domain + if locale: + locale = Locale.parse(locale) + self.locale = locale #: The locale or `None` + self._header_comment = header_comment + self._messages = odict() + + self.project = project or 'PROJECT' #: The project name + self.version = version or 'VERSION' #: The project version + self.copyright_holder = copyright_holder or 'ORGANIZATION' + self.msgid_bugs_address = msgid_bugs_address or 'EMAIL@ADDRESS' + + self.last_translator = last_translator or 'FULL NAME ' + """Name and email address of the last translator.""" + self.language_team = language_team or 'LANGUAGE ' + """Name and email address of the language team.""" + + self.charset = charset or 'utf-8' + + if creation_date is None: + creation_date = datetime.now(LOCALTZ) + elif isinstance(creation_date, datetime) and not creation_date.tzinfo: + creation_date = creation_date.replace(tzinfo=LOCALTZ) + self.creation_date = creation_date #: Creation date of the template + if revision_date is None: + revision_date = datetime.now(LOCALTZ) + elif isinstance(revision_date, datetime) and not revision_date.tzinfo: + revision_date = revision_date.replace(tzinfo=LOCALTZ) + self.revision_date = revision_date #: Last revision date of the catalog + self.fuzzy = fuzzy #: Catalog header fuzzy bit (`True` or `False`) + + self.obsolete = odict() #: Dictionary of obsolete messages + self._num_plurals = None + self._plural_expr = None + + def _get_header_comment(self): + comment = self._header_comment + comment = comment.replace('PROJECT', self.project) \ + .replace('VERSION', self.version) \ + .replace('YEAR', self.revision_date.strftime('%Y')) \ + .replace('ORGANIZATION', self.copyright_holder) + if self.locale: + comment = comment.replace('Translations template', '%s translations' + % self.locale.english_name) + return comment + + def _set_header_comment(self, string): + self._header_comment = string + + header_comment = property(_get_header_comment, _set_header_comment, doc="""\ + The header comment for the catalog. + + >>> catalog = Catalog(project='Foobar', version='1.0', + ... copyright_holder='Foo Company') + >>> print catalog.header_comment #doctest: +ELLIPSIS + # Translations template for Foobar. + # Copyright (C) ... Foo Company + # This file is distributed under the same license as the Foobar project. + # FIRST AUTHOR , .... + # + + The header can also be set from a string. Any known upper-case variables + will be replaced when the header is retrieved again: + + >>> catalog = Catalog(project='Foobar', version='1.0', + ... copyright_holder='Foo Company') + >>> catalog.header_comment = '''\\ + ... # The POT for my really cool PROJECT project. + ... # Copyright (C) 1990-2003 ORGANIZATION + ... # This file is distributed under the same license as the PROJECT + ... # project. + ... #''' + >>> print catalog.header_comment + # The POT for my really cool Foobar project. + # Copyright (C) 1990-2003 Foo Company + # This file is distributed under the same license as the Foobar + # project. + # + + :type: `unicode` + """) + + def _get_mime_headers(self): + headers = [] + headers.append(('Project-Id-Version', + '%s %s' % (self.project, self.version))) + headers.append(('Report-Msgid-Bugs-To', self.msgid_bugs_address)) + headers.append(('POT-Creation-Date', + format_datetime(self.creation_date, 'yyyy-MM-dd HH:mmZ', + locale='en'))) + if self.locale is None: + headers.append(('PO-Revision-Date', 'YEAR-MO-DA HO:MI+ZONE')) + headers.append(('Last-Translator', 'FULL NAME ')) + headers.append(('Language-Team', 'LANGUAGE ')) + else: + headers.append(('PO-Revision-Date', + format_datetime(self.revision_date, + 'yyyy-MM-dd HH:mmZ', locale='en'))) + headers.append(('Last-Translator', self.last_translator)) + headers.append(('Language-Team', + self.language_team.replace('LANGUAGE', + str(self.locale)))) + headers.append(('Plural-Forms', self.plural_forms)) + headers.append(('MIME-Version', '1.0')) + headers.append(('Content-Type', + 'text/plain; charset=%s' % self.charset)) + headers.append(('Content-Transfer-Encoding', '8bit')) + headers.append(('Generated-By', 'Babel %s\n' % VERSION)) + return headers + + def _set_mime_headers(self, headers): + for name, value in headers: + if name.lower() == 'content-type': + mimetype, params = parse_header(value) + if 'charset' in params: + self.charset = params['charset'].lower() + break + for name, value in headers: + name = name.lower().decode(self.charset) + value = value.decode(self.charset) + if name == 'project-id-version': + parts = value.split(' ') + self.project = u' '.join(parts[:-1]) + self.version = parts[-1] + elif name == 'report-msgid-bugs-to': + self.msgid_bugs_address = value + elif name == 'last-translator': + self.last_translator = value + elif name == 'language-team': + self.language_team = value + elif name == 'plural-forms': + _, params = parse_header(' ;' + value) + self._num_plurals = int(params.get('nplurals', 2)) + self._plural_expr = params.get('plural', '(n != 1)') + elif name == 'pot-creation-date': + # FIXME: this should use dates.parse_datetime as soon as that + # is ready + value, tzoffset, _ = re.split('([+-]\d{4})$', value, 1) + + tt = time.strptime(value, '%Y-%m-%d %H:%M') + ts = time.mktime(tt) + + # Separate the offset into a sign component, hours, and minutes + plus_minus_s, rest = tzoffset[0], tzoffset[1:] + hours_offset_s, mins_offset_s = rest[:2], rest[2:] + + # Make them all integers + plus_minus = int(plus_minus_s + '1') + hours_offset = int(hours_offset_s) + mins_offset = int(mins_offset_s) + + # Calculate net offset + net_mins_offset = hours_offset * 60 + net_mins_offset += mins_offset + net_mins_offset *= plus_minus + + # Create an offset object + tzoffset = FixedOffsetTimezone(net_mins_offset) + + # Store the offset in a datetime object + dt = datetime.fromtimestamp(ts) + self.creation_date = dt.replace(tzinfo=tzoffset) + elif name == 'po-revision-date': + # Keep the value if it's not the default one + if 'YEAR' not in value: + # FIXME: this should use dates.parse_datetime as soon as + # that is ready + value, tzoffset, _ = re.split('([+-]\d{4})$', value, 1) + tt = time.strptime(value, '%Y-%m-%d %H:%M') + ts = time.mktime(tt) + + # Separate the offset into a sign component, hours, and + # minutes + plus_minus_s, rest = tzoffset[0], tzoffset[1:] + hours_offset_s, mins_offset_s = rest[:2], rest[2:] + + # Make them all integers + plus_minus = int(plus_minus_s + '1') + hours_offset = int(hours_offset_s) + mins_offset = int(mins_offset_s) + + # Calculate net offset + net_mins_offset = hours_offset * 60 + net_mins_offset += mins_offset + net_mins_offset *= plus_minus + + # Create an offset object + tzoffset = FixedOffsetTimezone(net_mins_offset) + + # Store the offset in a datetime object + dt = datetime.fromtimestamp(ts) + self.revision_date = dt.replace(tzinfo=tzoffset) + + mime_headers = property(_get_mime_headers, _set_mime_headers, doc="""\ + The MIME headers of the catalog, used for the special ``msgid ""`` entry. + + The behavior of this property changes slightly depending on whether a locale + is set or not, the latter indicating that the catalog is actually a template + for actual translations. + + Here's an example of the output for such a catalog template: + + >>> created = datetime(1990, 4, 1, 15, 30, tzinfo=UTC) + >>> catalog = Catalog(project='Foobar', version='1.0', + ... creation_date=created) + >>> for name, value in catalog.mime_headers: + ... print '%s: %s' % (name, value) + Project-Id-Version: Foobar 1.0 + Report-Msgid-Bugs-To: EMAIL@ADDRESS + POT-Creation-Date: 1990-04-01 15:30+0000 + PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE + Last-Translator: FULL NAME + Language-Team: LANGUAGE + MIME-Version: 1.0 + Content-Type: text/plain; charset=utf-8 + Content-Transfer-Encoding: 8bit + Generated-By: Babel ... + + And here's an example of the output when the locale is set: + + >>> revised = datetime(1990, 8, 3, 12, 0, tzinfo=UTC) + >>> catalog = Catalog(locale='de_DE', project='Foobar', version='1.0', + ... creation_date=created, revision_date=revised, + ... last_translator='John Doe ', + ... language_team='de_DE ') + >>> for name, value in catalog.mime_headers: + ... print '%s: %s' % (name, value) + Project-Id-Version: Foobar 1.0 + Report-Msgid-Bugs-To: EMAIL@ADDRESS + POT-Creation-Date: 1990-04-01 15:30+0000 + PO-Revision-Date: 1990-08-03 12:00+0000 + Last-Translator: John Doe + Language-Team: de_DE + Plural-Forms: nplurals=2; plural=(n != 1) + MIME-Version: 1.0 + Content-Type: text/plain; charset=utf-8 + Content-Transfer-Encoding: 8bit + Generated-By: Babel ... + + :type: `list` + """) + + def num_plurals(self): + if self._num_plurals is None: + num = 2 + if self.locale: + num = get_plural(self.locale)[0] + self._num_plurals = num + return self._num_plurals + num_plurals = property(num_plurals, doc="""\ + The number of plurals used by the catalog or locale. + + >>> Catalog(locale='en').num_plurals + 2 + >>> Catalog(locale='ga').num_plurals + 3 + + :type: `int` + """) + + def plural_expr(self): + if self._plural_expr is None: + expr = '(n != 1)' + if self.locale: + expr = get_plural(self.locale)[1] + self._plural_expr = expr + return self._plural_expr + plural_expr = property(plural_expr, doc="""\ + The plural expression used by the catalog or locale. + + >>> Catalog(locale='en').plural_expr + '(n != 1)' + >>> Catalog(locale='ga').plural_expr + '(n==1 ? 0 : n==2 ? 1 : 2)' + + :type: `basestring` + """) + + def plural_forms(self): + return 'nplurals=%s; plural=%s' % (self.num_plurals, self.plural_expr) + plural_forms = property(plural_forms, doc="""\ + Return the plural forms declaration for the locale. + + >>> Catalog(locale='en').plural_forms + 'nplurals=2; plural=(n != 1)' + >>> Catalog(locale='pt_BR').plural_forms + 'nplurals=2; plural=(n > 1)' + + :type: `str` + """) + + def __contains__(self, id): + """Return whether the catalog has a message with the specified ID.""" + return self._key_for(id) in self._messages + + def __len__(self): + """The number of messages in the catalog. + + This does not include the special ``msgid ""`` entry. + """ + return len(self._messages) + + def __iter__(self): + """Iterates through all the entries in the catalog, in the order they + were added, yielding a `Message` object for every entry. + + :rtype: ``iterator`` + """ + buf = [] + for name, value in self.mime_headers: + buf.append('%s: %s' % (name, value)) + flags = set() + if self.fuzzy: + flags |= set(['fuzzy']) + yield Message(u'', '\n'.join(buf), flags=flags) + for key in self._messages: + yield self._messages[key] + + def __repr__(self): + locale = '' + if self.locale: + locale = ' %s' % self.locale + return '<%s %r%s>' % (type(self).__name__, self.domain, locale) + + def __delitem__(self, id): + """Delete the message with the specified ID.""" + key = self._key_for(id) + if key in self._messages: + del self._messages[key] + + def __getitem__(self, id): + """Return the message with the specified ID. + + :param id: the message ID + :return: the message with the specified ID, or `None` if no such message + is in the catalog + :rtype: `Message` + """ + return self._messages.get(self._key_for(id)) + + def __setitem__(self, id, message): + """Add or update the message with the specified ID. + + >>> catalog = Catalog() + >>> catalog[u'foo'] = Message(u'foo') + >>> catalog[u'foo'] + + + If a message with that ID is already in the catalog, it is updated + to include the locations and flags of the new message. + + >>> catalog = Catalog() + >>> catalog[u'foo'] = Message(u'foo', locations=[('main.py', 1)]) + >>> catalog[u'foo'].locations + [('main.py', 1)] + >>> catalog[u'foo'] = Message(u'foo', locations=[('utils.py', 5)]) + >>> catalog[u'foo'].locations + [('main.py', 1), ('utils.py', 5)] + + :param id: the message ID + :param message: the `Message` object + """ + assert isinstance(message, Message), 'expected a Message object' + key = self._key_for(id) + current = self._messages.get(key) + if current: + if message.pluralizable and not current.pluralizable: + # The new message adds pluralization + current.id = message.id + current.string = message.string + current.locations = list(distinct(current.locations + + message.locations)) + current.auto_comments = list(distinct(current.auto_comments + + message.auto_comments)) + current.user_comments = list(distinct(current.user_comments + + message.user_comments)) + current.flags |= message.flags + message = current + elif id == '': + # special treatment for the header message + headers = message_from_string(message.string.encode(self.charset)) + self.mime_headers = headers.items() + self.header_comment = '\n'.join(['# %s' % comment for comment + in message.user_comments]) + self.fuzzy = message.fuzzy + else: + if isinstance(id, (list, tuple)): + assert isinstance(message.string, (list, tuple)), \ + 'Expected sequence but got %s' % type(message.string) + self._messages[key] = message + + def add(self, id, string=None, locations=(), flags=(), auto_comments=(), + user_comments=(), previous_id=(), lineno=None): + """Add or update the message with the specified ID. + + >>> catalog = Catalog() + >>> catalog.add(u'foo') + >>> catalog[u'foo'] + + + This method simply constructs a `Message` object with the given + arguments and invokes `__setitem__` with that object. + + :param id: the message ID, or a ``(singular, plural)`` tuple for + pluralizable messages + :param string: the translated message string, or a + ``(singular, plural)`` tuple for pluralizable messages + :param locations: a sequence of ``(filenname, lineno)`` tuples + :param flags: a set or sequence of flags + :param auto_comments: a sequence of automatic comments + :param user_comments: a sequence of user comments + :param previous_id: the previous message ID, or a ``(singular, plural)`` + tuple for pluralizable messages + :param lineno: the line number on which the msgid line was found in the + PO file, if any + """ + self[id] = Message(id, string, list(locations), flags, auto_comments, + user_comments, previous_id, lineno=lineno) + + def check(self): + """Run various validation checks on the translations in the catalog. + + For every message which fails validation, this method yield a + ``(message, errors)`` tuple, where ``message`` is the `Message` object + and ``errors`` is a sequence of `TranslationError` objects. + + :rtype: ``iterator`` + """ + for message in self._messages.values(): + errors = message.check(catalog=self) + if errors: + yield message, errors + + def update(self, template, no_fuzzy_matching=False): + """Update the catalog based on the given template catalog. + + >>> from babel.messages import Catalog + >>> template = Catalog() + >>> template.add('green', locations=[('main.py', 99)]) + >>> template.add('blue', locations=[('main.py', 100)]) + >>> template.add(('salad', 'salads'), locations=[('util.py', 42)]) + >>> catalog = Catalog(locale='de_DE') + >>> catalog.add('blue', u'blau', locations=[('main.py', 98)]) + >>> catalog.add('head', u'Kopf', locations=[('util.py', 33)]) + >>> catalog.add(('salad', 'salads'), (u'Salat', u'Salate'), + ... locations=[('util.py', 38)]) + + >>> catalog.update(template) + >>> len(catalog) + 3 + + >>> msg1 = catalog['green'] + >>> msg1.string + >>> msg1.locations + [('main.py', 99)] + + >>> msg2 = catalog['blue'] + >>> msg2.string + u'blau' + >>> msg2.locations + [('main.py', 100)] + + >>> msg3 = catalog['salad'] + >>> msg3.string + (u'Salat', u'Salate') + >>> msg3.locations + [('util.py', 42)] + + Messages that are in the catalog but not in the template are removed + from the main collection, but can still be accessed via the `obsolete` + member: + + >>> 'head' in catalog + False + >>> catalog.obsolete.values() + [] + + :param template: the reference catalog, usually read from a POT file + :param no_fuzzy_matching: whether to use fuzzy matching of message IDs + """ + messages = self._messages + remaining = messages.copy() + self._messages = odict() + + # Prepare for fuzzy matching + fuzzy_candidates = [] + if not no_fuzzy_matching: + fuzzy_candidates = [ + self._key_for(msgid) for msgid in messages + if msgid and messages[msgid].string + ] + fuzzy_matches = set() + + def _merge(message, oldkey, newkey): + message = message.clone() + fuzzy = False + if oldkey != newkey: + fuzzy = True + fuzzy_matches.add(oldkey) + oldmsg = messages.get(oldkey) + if isinstance(oldmsg.id, basestring): + message.previous_id = [oldmsg.id] + else: + message.previous_id = list(oldmsg.id) + else: + oldmsg = remaining.pop(oldkey, None) + message.string = oldmsg.string + if isinstance(message.id, (list, tuple)): + if not isinstance(message.string, (list, tuple)): + fuzzy = True + message.string = tuple( + [message.string] + ([u''] * (len(message.id) - 1)) + ) + elif len(message.string) != self.num_plurals: + fuzzy = True + message.string = tuple(message.string[:len(oldmsg.string)]) + elif isinstance(message.string, (list, tuple)): + fuzzy = True + message.string = message.string[0] + message.flags |= oldmsg.flags + if fuzzy: + message.flags |= set([u'fuzzy']) + self[message.id] = message + + for message in template: + if message.id: + key = self._key_for(message.id) + if key in messages: + _merge(message, key, key) + else: + if no_fuzzy_matching is False: + # do some fuzzy matching with difflib + matches = get_close_matches(key.lower().strip(), + fuzzy_candidates, 1) + if matches: + _merge(message, matches[0], key) + continue + + self[message.id] = message + + self.obsolete = odict() + for msgid in remaining: + if no_fuzzy_matching or msgid not in fuzzy_matches: + self.obsolete[msgid] = remaining[msgid] + # Make updated catalog's POT-Creation-Date equal to the template + # used to update the catalog + self.creation_date = template.creation_date + + def _key_for(self, id): + """The key for a message is just the singular ID even for pluralizable + messages. + """ + key = id + if isinstance(key, (list, tuple)): + key = id[0] + return key diff --git a/justice/external/babel/messages/checkers.py b/justice/external/babel/messages/checkers.py index 3056998..82c0189 100644 --- a/justice/external/babel/messages/checkers.py +++ b/justice/external/babel/messages/checkers.py @@ -1,174 +1,174 @@ -# -*- 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 routines that help with validation of translations. - -:since: version 0.9 -""" - -from itertools import izip -from babel.messages.catalog import TranslationError, PYTHON_FORMAT -from babel.util import set - -#: list of format chars that are compatible to each other -_string_format_compatibilities = [ - set(['i', 'd', 'u']), - set(['x', 'X']), - set(['f', 'F', 'g', 'G']) -] - - -def num_plurals(catalog, message): - """Verify the number of plurals in the translation.""" - if not message.pluralizable: - if not isinstance(message.string, basestring): - raise TranslationError("Found plural forms for non-pluralizable " - "message") - return - - # skip further test if no catalog is provided. - elif catalog is None: - return - - msgstrs = message.string - if not isinstance(msgstrs, (list, tuple)): - msgstrs = (msgstrs,) - if len(msgstrs) != catalog.num_plurals: - raise TranslationError("Wrong number of plural forms (expected %d)" % - catalog.num_plurals) - - -def python_format(catalog, message): - """Verify the format string placeholders in the translation.""" - if 'python-format' not in message.flags: - return - msgids = message.id - if not isinstance(msgids, (list, tuple)): - msgids = (msgids,) - msgstrs = message.string - if not isinstance(msgstrs, (list, tuple)): - msgstrs = (msgstrs,) - - for msgid, msgstr in izip(msgids, msgstrs): - if msgstr: - _validate_format(msgid, msgstr) - - -def _validate_format(format, alternative): - """Test format string `alternative` against `format`. `format` can be the - msgid of a message and `alternative` one of the `msgstr`\s. The two - arguments are not interchangeable as `alternative` may contain less - placeholders if `format` uses named placeholders. - - The behavior of this function is undefined if the string does not use - string formattings. - - If the string formatting of `alternative` is compatible to `format` the - function returns `None`, otherwise a `TranslationError` is raised. - - Examples for compatible format strings: - - >>> _validate_format('Hello %s!', 'Hallo %s!') - >>> _validate_format('Hello %i!', 'Hallo %d!') - - Example for an incompatible format strings: - - >>> _validate_format('Hello %(name)s!', 'Hallo %s!') - Traceback (most recent call last): - ... - TranslationError: the format strings are of different kinds - - This function is used by the `python_format` checker. - - :param format: The original format string - :param alternative: The alternative format string that should be checked - against format - :return: None on success - :raises TranslationError: on formatting errors - """ - - def _parse(string): - result = [] - for match in PYTHON_FORMAT.finditer(string): - name, format, typechar = match.groups() - if typechar == '%' and name is None: - continue - result.append((name, str(typechar))) - return result - - def _compatible(a, b): - if a == b: - return True - for set in _string_format_compatibilities: - if a in set and b in set: - return True - return False - - def _check_positional(results): - positional = None - for name, char in results: - if positional is None: - positional = name is None - else: - if (name is None) != positional: - raise TranslationError('format string mixes positional ' - 'and named placeholders') - return bool(positional) - - a, b = map(_parse, (format, alternative)) - - # now check if both strings are positional or named - a_positional, b_positional = map(_check_positional, (a, b)) - if a_positional and not b_positional and not b: - raise TranslationError('placeholders are incompatible') - elif a_positional != b_positional: - raise TranslationError('the format strings are of different kinds') - - # if we are operating on positional strings both must have the - # same number of format chars and those must be compatible - if a_positional: - if len(a) != len(b): - raise TranslationError('positional format placeholders are ' - 'unbalanced') - for idx, ((_, first), (_, second)) in enumerate(izip(a, b)): - if not _compatible(first, second): - raise TranslationError('incompatible format for placeholder ' - '%d: %r and %r are not compatible' % - (idx + 1, first, second)) - - # otherwise the second string must not have names the first one - # doesn't have and the types of those included must be compatible - else: - type_map = dict(a) - for name, typechar in b: - if name not in type_map: - raise TranslationError('unknown named placeholder %r' % name) - elif not _compatible(typechar, type_map[name]): - raise TranslationError('incompatible format for ' - 'placeholder %r: ' - '%r and %r are not compatible' % - (name, typechar, type_map[name])) - - -def _find_checkers(): - try: - from pkg_resources import working_set - except ImportError: - return [num_plurals, python_format] - checkers = [] - for entry_point in working_set.iter_entry_points('babel.checkers'): - checkers.append(entry_point.load()) - return checkers - - -checkers = _find_checkers() +# -*- 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 routines that help with validation of translations. + +:since: version 0.9 +""" + +from itertools import izip +from babel.messages.catalog import TranslationError, PYTHON_FORMAT +from babel.util import set + +#: list of format chars that are compatible to each other +_string_format_compatibilities = [ + set(['i', 'd', 'u']), + set(['x', 'X']), + set(['f', 'F', 'g', 'G']) +] + + +def num_plurals(catalog, message): + """Verify the number of plurals in the translation.""" + if not message.pluralizable: + if not isinstance(message.string, basestring): + raise TranslationError("Found plural forms for non-pluralizable " + "message") + return + + # skip further test if no catalog is provided. + elif catalog is None: + return + + msgstrs = message.string + if not isinstance(msgstrs, (list, tuple)): + msgstrs = (msgstrs,) + if len(msgstrs) != catalog.num_plurals: + raise TranslationError("Wrong number of plural forms (expected %d)" % + catalog.num_plurals) + + +def python_format(catalog, message): + """Verify the format string placeholders in the translation.""" + if 'python-format' not in message.flags: + return + msgids = message.id + if not isinstance(msgids, (list, tuple)): + msgids = (msgids,) + msgstrs = message.string + if not isinstance(msgstrs, (list, tuple)): + msgstrs = (msgstrs,) + + for msgid, msgstr in izip(msgids, msgstrs): + if msgstr: + _validate_format(msgid, msgstr) + + +def _validate_format(format, alternative): + """Test format string `alternative` against `format`. `format` can be the + msgid of a message and `alternative` one of the `msgstr`\s. The two + arguments are not interchangeable as `alternative` may contain less + placeholders if `format` uses named placeholders. + + The behavior of this function is undefined if the string does not use + string formattings. + + If the string formatting of `alternative` is compatible to `format` the + function returns `None`, otherwise a `TranslationError` is raised. + + Examples for compatible format strings: + + >>> _validate_format('Hello %s!', 'Hallo %s!') + >>> _validate_format('Hello %i!', 'Hallo %d!') + + Example for an incompatible format strings: + + >>> _validate_format('Hello %(name)s!', 'Hallo %s!') + Traceback (most recent call last): + ... + TranslationError: the format strings are of different kinds + + This function is used by the `python_format` checker. + + :param format: The original format string + :param alternative: The alternative format string that should be checked + against format + :return: None on success + :raises TranslationError: on formatting errors + """ + + def _parse(string): + result = [] + for match in PYTHON_FORMAT.finditer(string): + name, format, typechar = match.groups() + if typechar == '%' and name is None: + continue + result.append((name, str(typechar))) + return result + + def _compatible(a, b): + if a == b: + return True + for set in _string_format_compatibilities: + if a in set and b in set: + return True + return False + + def _check_positional(results): + positional = None + for name, char in results: + if positional is None: + positional = name is None + else: + if (name is None) != positional: + raise TranslationError('format string mixes positional ' + 'and named placeholders') + return bool(positional) + + a, b = map(_parse, (format, alternative)) + + # now check if both strings are positional or named + a_positional, b_positional = map(_check_positional, (a, b)) + if a_positional and not b_positional and not b: + raise TranslationError('placeholders are incompatible') + elif a_positional != b_positional: + raise TranslationError('the format strings are of different kinds') + + # if we are operating on positional strings both must have the + # same number of format chars and those must be compatible + if a_positional: + if len(a) != len(b): + raise TranslationError('positional format placeholders are ' + 'unbalanced') + for idx, ((_, first), (_, second)) in enumerate(izip(a, b)): + if not _compatible(first, second): + raise TranslationError('incompatible format for placeholder ' + '%d: %r and %r are not compatible' % + (idx + 1, first, second)) + + # otherwise the second string must not have names the first one + # doesn't have and the types of those included must be compatible + else: + type_map = dict(a) + for name, typechar in b: + if name not in type_map: + raise TranslationError('unknown named placeholder %r' % name) + elif not _compatible(typechar, type_map[name]): + raise TranslationError('incompatible format for ' + 'placeholder %r: ' + '%r and %r are not compatible' % + (name, typechar, type_map[name])) + + +def _find_checkers(): + try: + from pkg_resources import working_set + except ImportError: + return [num_plurals, python_format] + checkers = [] + for entry_point in working_set.iter_entry_points('babel.checkers'): + checkers.append(entry_point.load()) + return checkers + + +checkers = _find_checkers() diff --git a/justice/external/babel/messages/extract.py b/justice/external/babel/messages/extract.py index f7813ce..e7cf696 100644 --- a/justice/external/babel/messages/extract.py +++ b/justice/external/babel/messages/extract.py @@ -1,554 +1,554 @@ -# -*- 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 -try: - set -except NameError: - from sets import Set as set -import sys -from tokenize import generate_tokens, COMMENT, NAME, OP, STRING - -from babel.util import parse_encoding, pathmatch, relpath -from textwrap import dedent - -__all__ = ['extract', 'extract_from_dir', 'extract_from_file'] -__docformat__ = 'restructuredtext en' - -GROUP_NAME = 'babel.extractors' - -DEFAULT_KEYWORDS = { - '_': None, - 'gettext': None, - 'ngettext': (1, 2), - 'ugettext': None, - 'ungettext': (1, 2), - 'dgettext': (2,), - 'dngettext': (2, 3), - 'N_': None -} - -DEFAULT_MAPPING = [('**.py', 'python')] - -empty_msgid_warning = ( -'%s: warning: Empty msgid. It is reserved by GNU gettext: gettext("") ' -'returns the header entry with meta information, not the empty string.') - - -def _strip_comment_tags(comments, tags): - """Helper function for `extract` that strips comment tags from strings - in a list of comment lines. This functions operates in-place. - """ - def _strip(line): - for tag in tags: - if line.startswith(tag): - return line[len(tag):].strip() - return line - comments[:] = map(_strip, comments) - - -def extract_from_dir(dirname=os.getcwd(), method_map=DEFAULT_MAPPING, - options_map=None, keywords=DEFAULT_KEYWORDS, - comment_tags=(), callback=None, strip_comment_tags=False): - """Extract messages from any source files found in the given directory. - - This function generates tuples of the form: - - ``(filename, lineno, message, comments)`` - - Which extraction method is used per file is determined by the `method_map` - parameter, which maps extended glob patterns to extraction method names. - For example, the following is the default mapping: - - >>> method_map = [ - ... ('**.py', 'python') - ... ] - - 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 mapping patterns are ignored. See - the documentation of the `pathmatch` function for details on the pattern - syntax. - - The following extended mapping would also use the "genshi" extraction - method on any file in "templates" subdirectory: - - >>> method_map = [ - ... ('**/templates/**.*', 'genshi'), - ... ('**.py', 'python') - ... ] - - The dictionary provided by the optional `options_map` parameter augments - these mappings. It uses extended glob patterns as keys, and the values are - dictionaries mapping options names to option values (both strings). - - The glob patterns of the `options_map` do not necessarily need to be the - same as those used in the method mapping. For example, while all files in - the ``templates`` folders in an application may be Genshi applications, the - options for those files may differ based on extension: - - >>> options_map = { - ... '**/templates/**.txt': { - ... 'template_class': 'genshi.template:TextTemplate', - ... 'encoding': 'latin-1' - ... }, - ... '**/templates/**.html': { - ... 'include_attrs': '' - ... } - ... } - - :param dirname: the path to the directory to extract messages from - :param method_map: a list of ``(pattern, method)`` tuples that maps of - extraction method names to extended glob patterns - :param options_map: a dictionary of additional options (optional) - :param keywords: a dictionary mapping keywords (i.e. names of functions - that should be recognized as translation functions) to - tuples that specify which of their arguments contain - localizable strings - :param comment_tags: a list of tags of translator comments to search for - and include in the results - :param callback: a function that is called for every file that message are - extracted from, just before the extraction itself is - performed; the function is passed the filename, the name - of the extraction method and and the options dictionary as - positional arguments, in that order - :param strip_comment_tags: a flag that if set to `True` causes all comment - tags to be removed from the collected comments. - :return: an iterator over ``(filename, lineno, funcname, message)`` tuples - :rtype: ``iterator`` - :see: `pathmatch` - """ - if options_map is None: - options_map = {} - - 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) - dirnames.sort() - filenames.sort() - for filename in filenames: - filename = relpath( - os.path.join(root, filename).replace(os.sep, '/'), - dirname - ) - for pattern, method in method_map: - if pathmatch(pattern, filename): - filepath = os.path.join(absname, filename) - options = {} - for opattern, odict in options_map.items(): - if pathmatch(opattern, filename): - options = odict - if callback: - callback(filename, method, options) - for lineno, message, comments in \ - extract_from_file(method, filepath, - keywords=keywords, - comment_tags=comment_tags, - options=options, - strip_comment_tags= - strip_comment_tags): - yield filename, lineno, message, comments - break - - -def extract_from_file(method, filename, keywords=DEFAULT_KEYWORDS, - comment_tags=(), options=None, strip_comment_tags=False): - """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 dictionary mapping keywords (i.e. names of functions - that should be recognized as translation functions) to - tuples that specify which of their arguments contain - localizable strings - :param comment_tags: a list of translator tags to search for and include - in the results - :param strip_comment_tags: a flag that if set to `True` causes all comment - tags to be removed from the collected comments. - :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, comment_tags, options, - strip_comment_tags)) - finally: - fileobj.close() - - -def extract(method, fileobj, keywords=DEFAULT_KEYWORDS, comment_tags=(), - options=None, strip_comment_tags=False): - """Extract messages from the given file-like object using the specified - extraction method. - - This function returns a list of tuples of the form: - - ``(lineno, message, comments)`` - - 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, u'Hello, world!', []) - - :param method: a string specifying the extraction method (.e.g. "python"); - if this is a simple name, the extraction function will be - looked up by entry point; if it is an explicit reference - to a function (of the form ``package.module:funcname`` or - ``package.module.funcname``), the corresponding function - will be imported and used - :param fileobj: the file-like object the messages should be extracted from - :param keywords: a dictionary mapping keywords (i.e. names of functions - that should be recognized as translation functions) to - tuples that specify which of their arguments contain - localizable strings - :param comment_tags: a list of translator tags to search for and include - in the results - :param options: a dictionary of additional options (optional) - :param strip_comment_tags: a flag that if set to `True` causes all comment - tags to be removed from the collected comments. - :return: the list of extracted messages - :rtype: `list` - :raise ValueError: if the extraction method is not registered - """ - func = None - if ':' in method or '.' in method: - if ':' not in method: - lastdot = method.rfind('.') - module, attrname = method[:lastdot], method[lastdot + 1:] - else: - module, attrname = method.split(':', 1) - func = getattr(__import__(module, {}, {}, [attrname]), attrname) - else: - try: - from pkg_resources import working_set - except ImportError: - # pkg_resources is not available, so we resort to looking up the - # builtin extractors directly - builtin = {'ignore': extract_nothing, 'python': extract_python} - func = builtin.get(method) - else: - for entry_point in working_set.iter_entry_points(GROUP_NAME, - method): - func = entry_point.load(require=True) - break - if func is None: - raise ValueError('Unknown extraction method %r' % method) - - results = func(fileobj, keywords.keys(), comment_tags, - options=options or {}) - - for lineno, funcname, messages, comments in results: - if funcname: - spec = keywords[funcname] or (1,) - else: - spec = (1,) - if not isinstance(messages, (list, tuple)): - messages = [messages] - if not messages: - continue - - # Validate the messages against the keyword's specification - msgs = [] - invalid = False - # last_index is 1 based like the keyword spec - last_index = len(messages) - for index in spec: - if last_index < index: - # Not enough arguments - invalid = True - break - message = messages[index - 1] - if message is None: - invalid = True - break - msgs.append(message) - if invalid: - continue - - first_msg_index = spec[0] - 1 - if not messages[first_msg_index]: - # An empty string msgid isn't valid, emit a warning - where = '%s:%i' % (hasattr(fileobj, 'name') and \ - fileobj.name or '(unknown)', lineno) - print >> sys.stderr, empty_msgid_warning % where - continue - - messages = tuple(msgs) - if len(messages) == 1: - messages = messages[0] - - if strip_comment_tags: - _strip_comment_tags(comments, comment_tags) - yield lineno, messages, comments - - -def extract_nothing(fileobj, keywords, comment_tags, options): - """Pseudo extractor that does not actually extract anything, but simply - returns an empty list. - """ - return [] - - -def extract_python(fileobj, keywords, comment_tags, options): - """Extract messages from Python source code. - - :param fileobj: the seekable, 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 comment_tags: a list of translator tags to search for and include - in the results - :param options: a dictionary of additional options (optional) - :return: an iterator over ``(lineno, funcname, message, comments)`` tuples - :rtype: ``iterator`` - """ - funcname = lineno = message_lineno = None - call_stack = -1 - buf = [] - messages = [] - translator_comments = [] - in_def = in_translator_comments = False - comment_tag = None - - encoding = parse_encoding(fileobj) or options.get('encoding', 'iso-8859-1') - - tokens = generate_tokens(fileobj.readline) - for tok, value, (lineno, _), _, _ in tokens: - if call_stack == -1 and tok == NAME and value in ('def', 'class'): - in_def = True - elif tok == OP and value == '(': - if in_def: - # Avoid false positives for declarations such as: - # def gettext(arg='message'): - in_def = False - continue - if funcname: - message_lineno = lineno - call_stack += 1 - elif in_def and tok == OP and value == ':': - # End of a class definition without parens - in_def = False - continue - elif call_stack == -1 and tok == COMMENT: - # Strip the comment token from the line - value = value.decode(encoding)[1:].strip() - if in_translator_comments and \ - translator_comments[-1][0] == lineno - 1: - # We're already inside a translator comment, continue appending - translator_comments.append((lineno, value)) - continue - # If execution reaches this point, let's see if comment line - # starts with one of the comment tags - for comment_tag in comment_tags: - if value.startswith(comment_tag): - in_translator_comments = True - translator_comments.append((lineno, value)) - break - elif funcname and call_stack == 0: - if tok == OP and value == ')': - if buf: - messages.append(''.join(buf)) - del buf[:] - else: - messages.append(None) - - if len(messages) > 1: - messages = tuple(messages) - else: - messages = messages[0] - # Comments don't apply unless they immediately preceed the - # message - if translator_comments and \ - translator_comments[-1][0] < message_lineno - 1: - translator_comments = [] - - yield (message_lineno, funcname, messages, - [comment[1] for comment in translator_comments]) - - funcname = lineno = message_lineno = None - call_stack = -1 - messages = [] - translator_comments = [] - in_translator_comments = False - elif tok == STRING: - # Unwrap quotes in a safe manner, maintaining the string's - # encoding - # https://sourceforge.net/tracker/?func=detail&atid=355470& - # aid=617979&group_id=5470 - value = eval('# coding=%s\n%s' % (encoding, value), - {'__builtins__':{}}, {}) - if isinstance(value, str): - value = value.decode(encoding) - buf.append(value) - elif tok == OP and value == ',': - if buf: - messages.append(''.join(buf)) - del buf[:] - else: - messages.append(None) - if translator_comments: - # We have translator comments, and since we're on a - # comma(,) user is allowed to break into a new line - # Let's increase the last comment's lineno in order - # for the comment to still be a valid one - old_lineno, old_comment = translator_comments.pop() - translator_comments.append((old_lineno+1, old_comment)) - elif call_stack > 0 and tok == OP and value == ')': - call_stack -= 1 - elif funcname and call_stack == -1: - funcname = None - elif tok == NAME and value in keywords: - funcname = value - - -def extract_javascript(fileobj, keywords, comment_tags, options): - """Extract messages from JavaScript source code. - - :param fileobj: the seekable, 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 comment_tags: a list of translator tags to search for and include - in the results - :param options: a dictionary of additional options (optional) - :return: an iterator over ``(lineno, funcname, message, comments)`` tuples - :rtype: ``iterator`` - """ - from babel.messages.jslexer import tokenize, unquote_string - funcname = message_lineno = None - messages = [] - last_argument = None - translator_comments = [] - concatenate_next = False - encoding = options.get('encoding', 'utf-8') - last_token = None - call_stack = -1 - - for token in tokenize(fileobj.read().decode(encoding)): - if token.type == 'operator' and token.value == '(': - if funcname: - message_lineno = token.lineno - call_stack += 1 - - elif call_stack == -1 and token.type == 'linecomment': - value = token.value[2:].strip() - if translator_comments and \ - translator_comments[-1][0] == token.lineno - 1: - translator_comments.append((token.lineno, value)) - continue - - for comment_tag in comment_tags: - if value.startswith(comment_tag): - translator_comments.append((token.lineno, value.strip())) - break - - elif token.type == 'multilinecomment': - # only one multi-line comment may preceed a translation - translator_comments = [] - value = token.value[2:-2].strip() - for comment_tag in comment_tags: - if value.startswith(comment_tag): - lines = value.splitlines() - if lines: - lines[0] = lines[0].strip() - lines[1:] = dedent('\n'.join(lines[1:])).splitlines() - for offset, line in enumerate(lines): - translator_comments.append((token.lineno + offset, - line)) - break - - elif funcname and call_stack == 0: - if token.type == 'operator' and token.value == ')': - if last_argument is not None: - messages.append(last_argument) - if len(messages) > 1: - messages = tuple(messages) - elif messages: - messages = messages[0] - else: - messages = None - - # Comments don't apply unless they immediately precede the - # message - if translator_comments and \ - translator_comments[-1][0] < message_lineno - 1: - translator_comments = [] - - if messages is not None: - yield (message_lineno, funcname, messages, - [comment[1] for comment in translator_comments]) - - funcname = message_lineno = last_argument = None - concatenate_next = False - translator_comments = [] - messages = [] - call_stack = -1 - - elif token.type == 'string': - new_value = unquote_string(token.value) - if concatenate_next: - last_argument = (last_argument or '') + new_value - concatenate_next = False - else: - last_argument = new_value - - elif token.type == 'operator': - if token.value == ',': - if last_argument is not None: - messages.append(last_argument) - last_argument = None - else: - messages.append(None) - concatenate_next = False - elif token.value == '+': - concatenate_next = True - - elif call_stack > 0 and token.type == 'operator' \ - and token.value == ')': - call_stack -= 1 - - elif funcname and call_stack == -1: - funcname = None - - elif call_stack == -1 and token.type == 'name' and \ - token.value in keywords and \ - (last_token is None or last_token.type != 'name' or - last_token.value != 'function'): - funcname = token.value - - last_token = token +# -*- 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 +try: + set +except NameError: + from sets import Set as set +import sys +from tokenize import generate_tokens, COMMENT, NAME, OP, STRING + +from babel.util import parse_encoding, pathmatch, relpath +from textwrap import dedent + +__all__ = ['extract', 'extract_from_dir', 'extract_from_file'] +__docformat__ = 'restructuredtext en' + +GROUP_NAME = 'babel.extractors' + +DEFAULT_KEYWORDS = { + '_': None, + 'gettext': None, + 'ngettext': (1, 2), + 'ugettext': None, + 'ungettext': (1, 2), + 'dgettext': (2,), + 'dngettext': (2, 3), + 'N_': None +} + +DEFAULT_MAPPING = [('**.py', 'python')] + +empty_msgid_warning = ( +'%s: warning: Empty msgid. It is reserved by GNU gettext: gettext("") ' +'returns the header entry with meta information, not the empty string.') + + +def _strip_comment_tags(comments, tags): + """Helper function for `extract` that strips comment tags from strings + in a list of comment lines. This functions operates in-place. + """ + def _strip(line): + for tag in tags: + if line.startswith(tag): + return line[len(tag):].strip() + return line + comments[:] = map(_strip, comments) + + +def extract_from_dir(dirname=os.getcwd(), method_map=DEFAULT_MAPPING, + options_map=None, keywords=DEFAULT_KEYWORDS, + comment_tags=(), callback=None, strip_comment_tags=False): + """Extract messages from any source files found in the given directory. + + This function generates tuples of the form: + + ``(filename, lineno, message, comments)`` + + Which extraction method is used per file is determined by the `method_map` + parameter, which maps extended glob patterns to extraction method names. + For example, the following is the default mapping: + + >>> method_map = [ + ... ('**.py', 'python') + ... ] + + 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 mapping patterns are ignored. See + the documentation of the `pathmatch` function for details on the pattern + syntax. + + The following extended mapping would also use the "genshi" extraction + method on any file in "templates" subdirectory: + + >>> method_map = [ + ... ('**/templates/**.*', 'genshi'), + ... ('**.py', 'python') + ... ] + + The dictionary provided by the optional `options_map` parameter augments + these mappings. It uses extended glob patterns as keys, and the values are + dictionaries mapping options names to option values (both strings). + + The glob patterns of the `options_map` do not necessarily need to be the + same as those used in the method mapping. For example, while all files in + the ``templates`` folders in an application may be Genshi applications, the + options for those files may differ based on extension: + + >>> options_map = { + ... '**/templates/**.txt': { + ... 'template_class': 'genshi.template:TextTemplate', + ... 'encoding': 'latin-1' + ... }, + ... '**/templates/**.html': { + ... 'include_attrs': '' + ... } + ... } + + :param dirname: the path to the directory to extract messages from + :param method_map: a list of ``(pattern, method)`` tuples that maps of + extraction method names to extended glob patterns + :param options_map: a dictionary of additional options (optional) + :param keywords: a dictionary mapping keywords (i.e. names of functions + that should be recognized as translation functions) to + tuples that specify which of their arguments contain + localizable strings + :param comment_tags: a list of tags of translator comments to search for + and include in the results + :param callback: a function that is called for every file that message are + extracted from, just before the extraction itself is + performed; the function is passed the filename, the name + of the extraction method and and the options dictionary as + positional arguments, in that order + :param strip_comment_tags: a flag that if set to `True` causes all comment + tags to be removed from the collected comments. + :return: an iterator over ``(filename, lineno, funcname, message)`` tuples + :rtype: ``iterator`` + :see: `pathmatch` + """ + if options_map is None: + options_map = {} + + 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) + dirnames.sort() + filenames.sort() + for filename in filenames: + filename = relpath( + os.path.join(root, filename).replace(os.sep, '/'), + dirname + ) + for pattern, method in method_map: + if pathmatch(pattern, filename): + filepath = os.path.join(absname, filename) + options = {} + for opattern, odict in options_map.items(): + if pathmatch(opattern, filename): + options = odict + if callback: + callback(filename, method, options) + for lineno, message, comments in \ + extract_from_file(method, filepath, + keywords=keywords, + comment_tags=comment_tags, + options=options, + strip_comment_tags= + strip_comment_tags): + yield filename, lineno, message, comments + break + + +def extract_from_file(method, filename, keywords=DEFAULT_KEYWORDS, + comment_tags=(), options=None, strip_comment_tags=False): + """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 dictionary mapping keywords (i.e. names of functions + that should be recognized as translation functions) to + tuples that specify which of their arguments contain + localizable strings + :param comment_tags: a list of translator tags to search for and include + in the results + :param strip_comment_tags: a flag that if set to `True` causes all comment + tags to be removed from the collected comments. + :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, comment_tags, options, + strip_comment_tags)) + finally: + fileobj.close() + + +def extract(method, fileobj, keywords=DEFAULT_KEYWORDS, comment_tags=(), + options=None, strip_comment_tags=False): + """Extract messages from the given file-like object using the specified + extraction method. + + This function returns a list of tuples of the form: + + ``(lineno, message, comments)`` + + 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, u'Hello, world!', []) + + :param method: a string specifying the extraction method (.e.g. "python"); + if this is a simple name, the extraction function will be + looked up by entry point; if it is an explicit reference + to a function (of the form ``package.module:funcname`` or + ``package.module.funcname``), the corresponding function + will be imported and used + :param fileobj: the file-like object the messages should be extracted from + :param keywords: a dictionary mapping keywords (i.e. names of functions + that should be recognized as translation functions) to + tuples that specify which of their arguments contain + localizable strings + :param comment_tags: a list of translator tags to search for and include + in the results + :param options: a dictionary of additional options (optional) + :param strip_comment_tags: a flag that if set to `True` causes all comment + tags to be removed from the collected comments. + :return: the list of extracted messages + :rtype: `list` + :raise ValueError: if the extraction method is not registered + """ + func = None + if ':' in method or '.' in method: + if ':' not in method: + lastdot = method.rfind('.') + module, attrname = method[:lastdot], method[lastdot + 1:] + else: + module, attrname = method.split(':', 1) + func = getattr(__import__(module, {}, {}, [attrname]), attrname) + else: + try: + from pkg_resources import working_set + except ImportError: + # pkg_resources is not available, so we resort to looking up the + # builtin extractors directly + builtin = {'ignore': extract_nothing, 'python': extract_python} + func = builtin.get(method) + else: + for entry_point in working_set.iter_entry_points(GROUP_NAME, + method): + func = entry_point.load(require=True) + break + if func is None: + raise ValueError('Unknown extraction method %r' % method) + + results = func(fileobj, keywords.keys(), comment_tags, + options=options or {}) + + for lineno, funcname, messages, comments in results: + if funcname: + spec = keywords[funcname] or (1,) + else: + spec = (1,) + if not isinstance(messages, (list, tuple)): + messages = [messages] + if not messages: + continue + + # Validate the messages against the keyword's specification + msgs = [] + invalid = False + # last_index is 1 based like the keyword spec + last_index = len(messages) + for index in spec: + if last_index < index: + # Not enough arguments + invalid = True + break + message = messages[index - 1] + if message is None: + invalid = True + break + msgs.append(message) + if invalid: + continue + + first_msg_index = spec[0] - 1 + if not messages[first_msg_index]: + # An empty string msgid isn't valid, emit a warning + where = '%s:%i' % (hasattr(fileobj, 'name') and \ + fileobj.name or '(unknown)', lineno) + print >> sys.stderr, empty_msgid_warning % where + continue + + messages = tuple(msgs) + if len(messages) == 1: + messages = messages[0] + + if strip_comment_tags: + _strip_comment_tags(comments, comment_tags) + yield lineno, messages, comments + + +def extract_nothing(fileobj, keywords, comment_tags, options): + """Pseudo extractor that does not actually extract anything, but simply + returns an empty list. + """ + return [] + + +def extract_python(fileobj, keywords, comment_tags, options): + """Extract messages from Python source code. + + :param fileobj: the seekable, 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 comment_tags: a list of translator tags to search for and include + in the results + :param options: a dictionary of additional options (optional) + :return: an iterator over ``(lineno, funcname, message, comments)`` tuples + :rtype: ``iterator`` + """ + funcname = lineno = message_lineno = None + call_stack = -1 + buf = [] + messages = [] + translator_comments = [] + in_def = in_translator_comments = False + comment_tag = None + + encoding = parse_encoding(fileobj) or options.get('encoding', 'iso-8859-1') + + tokens = generate_tokens(fileobj.readline) + for tok, value, (lineno, _), _, _ in tokens: + if call_stack == -1 and tok == NAME and value in ('def', 'class'): + in_def = True + elif tok == OP and value == '(': + if in_def: + # Avoid false positives for declarations such as: + # def gettext(arg='message'): + in_def = False + continue + if funcname: + message_lineno = lineno + call_stack += 1 + elif in_def and tok == OP and value == ':': + # End of a class definition without parens + in_def = False + continue + elif call_stack == -1 and tok == COMMENT: + # Strip the comment token from the line + value = value.decode(encoding)[1:].strip() + if in_translator_comments and \ + translator_comments[-1][0] == lineno - 1: + # We're already inside a translator comment, continue appending + translator_comments.append((lineno, value)) + continue + # If execution reaches this point, let's see if comment line + # starts with one of the comment tags + for comment_tag in comment_tags: + if value.startswith(comment_tag): + in_translator_comments = True + translator_comments.append((lineno, value)) + break + elif funcname and call_stack == 0: + if tok == OP and value == ')': + if buf: + messages.append(''.join(buf)) + del buf[:] + else: + messages.append(None) + + if len(messages) > 1: + messages = tuple(messages) + else: + messages = messages[0] + # Comments don't apply unless they immediately preceed the + # message + if translator_comments and \ + translator_comments[-1][0] < message_lineno - 1: + translator_comments = [] + + yield (message_lineno, funcname, messages, + [comment[1] for comment in translator_comments]) + + funcname = lineno = message_lineno = None + call_stack = -1 + messages = [] + translator_comments = [] + in_translator_comments = False + elif tok == STRING: + # Unwrap quotes in a safe manner, maintaining the string's + # encoding + # https://sourceforge.net/tracker/?func=detail&atid=355470& + # aid=617979&group_id=5470 + value = eval('# coding=%s\n%s' % (encoding, value), + {'__builtins__':{}}, {}) + if isinstance(value, str): + value = value.decode(encoding) + buf.append(value) + elif tok == OP and value == ',': + if buf: + messages.append(''.join(buf)) + del buf[:] + else: + messages.append(None) + if translator_comments: + # We have translator comments, and since we're on a + # comma(,) user is allowed to break into a new line + # Let's increase the last comment's lineno in order + # for the comment to still be a valid one + old_lineno, old_comment = translator_comments.pop() + translator_comments.append((old_lineno+1, old_comment)) + elif call_stack > 0 and tok == OP and value == ')': + call_stack -= 1 + elif funcname and call_stack == -1: + funcname = None + elif tok == NAME and value in keywords: + funcname = value + + +def extract_javascript(fileobj, keywords, comment_tags, options): + """Extract messages from JavaScript source code. + + :param fileobj: the seekable, 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 comment_tags: a list of translator tags to search for and include + in the results + :param options: a dictionary of additional options (optional) + :return: an iterator over ``(lineno, funcname, message, comments)`` tuples + :rtype: ``iterator`` + """ + from babel.messages.jslexer import tokenize, unquote_string + funcname = message_lineno = None + messages = [] + last_argument = None + translator_comments = [] + concatenate_next = False + encoding = options.get('encoding', 'utf-8') + last_token = None + call_stack = -1 + + for token in tokenize(fileobj.read().decode(encoding)): + if token.type == 'operator' and token.value == '(': + if funcname: + message_lineno = token.lineno + call_stack += 1 + + elif call_stack == -1 and token.type == 'linecomment': + value = token.value[2:].strip() + if translator_comments and \ + translator_comments[-1][0] == token.lineno - 1: + translator_comments.append((token.lineno, value)) + continue + + for comment_tag in comment_tags: + if value.startswith(comment_tag): + translator_comments.append((token.lineno, value.strip())) + break + + elif token.type == 'multilinecomment': + # only one multi-line comment may preceed a translation + translator_comments = [] + value = token.value[2:-2].strip() + for comment_tag in comment_tags: + if value.startswith(comment_tag): + lines = value.splitlines() + if lines: + lines[0] = lines[0].strip() + lines[1:] = dedent('\n'.join(lines[1:])).splitlines() + for offset, line in enumerate(lines): + translator_comments.append((token.lineno + offset, + line)) + break + + elif funcname and call_stack == 0: + if token.type == 'operator' and token.value == ')': + if last_argument is not None: + messages.append(last_argument) + if len(messages) > 1: + messages = tuple(messages) + elif messages: + messages = messages[0] + else: + messages = None + + # Comments don't apply unless they immediately precede the + # message + if translator_comments and \ + translator_comments[-1][0] < message_lineno - 1: + translator_comments = [] + + if messages is not None: + yield (message_lineno, funcname, messages, + [comment[1] for comment in translator_comments]) + + funcname = message_lineno = last_argument = None + concatenate_next = False + translator_comments = [] + messages = [] + call_stack = -1 + + elif token.type == 'string': + new_value = unquote_string(token.value) + if concatenate_next: + last_argument = (last_argument or '') + new_value + concatenate_next = False + else: + last_argument = new_value + + elif token.type == 'operator': + if token.value == ',': + if last_argument is not None: + messages.append(last_argument) + last_argument = None + else: + messages.append(None) + concatenate_next = False + elif token.value == '+': + concatenate_next = True + + elif call_stack > 0 and token.type == 'operator' \ + and token.value == ')': + call_stack -= 1 + + elif funcname and call_stack == -1: + funcname = None + + elif call_stack == -1 and token.type == 'name' and \ + token.value in keywords and \ + (last_token is None or last_token.type != 'name' or + last_token.value != 'function'): + funcname = token.value + + last_token = token diff --git a/justice/external/babel/messages/frontend.py b/justice/external/babel/messages/frontend.py index bdcf942..2328aae 100644 --- a/justice/external/babel/messages/frontend.py +++ b/justice/external/babel/messages/frontend.py @@ -1,1208 +1,1208 @@ -#!/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/. - -"""Frontends for the message extraction functionality.""" - -from ConfigParser import RawConfigParser -from datetime import datetime -from distutils import log -from distutils.cmd import Command -from distutils.errors import DistutilsOptionError, DistutilsSetupError -from locale import getpreferredencoding -import logging -from optparse import OptionParser -import os -import re -import shutil -from StringIO import StringIO -import sys -import tempfile - -from babel import __version__ as VERSION -from babel import Locale, localedata -from babel.core import UnknownLocaleError -from babel.messages.catalog import Catalog -from babel.messages.extract import extract_from_dir, DEFAULT_KEYWORDS, \ - DEFAULT_MAPPING -from babel.messages.mofile import write_mo -from babel.messages.pofile import read_po, write_po -from babel.messages.plurals import PLURALS -from babel.util import odict, LOCALTZ - -__all__ = ['CommandLineInterface', 'compile_catalog', 'extract_messages', - 'init_catalog', 'check_message_extractors', 'update_catalog'] -__docformat__ = 'restructuredtext en' - - -class compile_catalog(Command): - """Catalog compilation 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.messages.frontend import compile_catalog - - setup( - ... - cmdclass = {'compile_catalog': compile_catalog} - ) - - :since: version 0.9 - :see: `Integrating new distutils commands `_ - :see: `setuptools `_ - """ - - description = 'compile message catalogs to binary MO files' - user_options = [ - ('domain=', 'D', - "domain of PO file (default 'messages')"), - ('directory=', 'd', - 'path to base directory containing the catalogs'), - ('input-file=', 'i', - 'name of the input file'), - ('output-file=', 'o', - "name of the output file (default " - "'//LC_MESSAGES/.po')"), - ('locale=', 'l', - 'locale of the catalog to compile'), - ('use-fuzzy', 'f', - 'also include fuzzy translations'), - ('statistics', None, - 'print statistics about translations') - ] - boolean_options = ['use-fuzzy', 'statistics'] - - def initialize_options(self): - self.domain = 'messages' - self.directory = None - self.input_file = None - self.output_file = None - self.locale = None - self.use_fuzzy = False - self.statistics = False - - def finalize_options(self): - if not self.input_file and not self.directory: - raise DistutilsOptionError('you must specify either the input file ' - 'or the base directory') - if not self.output_file and not self.directory: - raise DistutilsOptionError('you must specify either the input file ' - 'or the base directory') - - def run(self): - po_files = [] - mo_files = [] - - if not self.input_file: - if self.locale: - po_files.append((self.locale, - os.path.join(self.directory, self.locale, - 'LC_MESSAGES', - self.domain + '.po'))) - mo_files.append(os.path.join(self.directory, self.locale, - 'LC_MESSAGES', - self.domain + '.mo')) - else: - for locale in os.listdir(self.directory): - po_file = os.path.join(self.directory, locale, - 'LC_MESSAGES', self.domain + '.po') - if os.path.exists(po_file): - po_files.append((locale, po_file)) - mo_files.append(os.path.join(self.directory, locale, - 'LC_MESSAGES', - self.domain + '.mo')) - else: - po_files.append((self.locale, self.input_file)) - if self.output_file: - mo_files.append(self.output_file) - else: - mo_files.append(os.path.join(self.directory, self.locale, - 'LC_MESSAGES', - self.domain + '.mo')) - - if not po_files: - raise DistutilsOptionError('no message catalogs found') - - for idx, (locale, po_file) in enumerate(po_files): - mo_file = mo_files[idx] - infile = open(po_file, 'r') - try: - catalog = read_po(infile, locale) - finally: - infile.close() - - if self.statistics: - translated = 0 - for message in list(catalog)[1:]: - if message.string: - translated +=1 - percentage = 0 - if len(catalog): - percentage = translated * 100 // len(catalog) - log.info('%d of %d messages (%d%%) translated in %r', - translated, len(catalog), percentage, po_file) - - if catalog.fuzzy and not self.use_fuzzy: - log.warn('catalog %r is marked as fuzzy, skipping', po_file) - continue - - for message, errors in catalog.check(): - for error in errors: - log.error('error: %s:%d: %s', po_file, message.lineno, - error) - - log.info('compiling catalog %r to %r', po_file, mo_file) - - outfile = open(mo_file, 'wb') - try: - write_mo(outfile, catalog, use_fuzzy=self.use_fuzzy) - finally: - outfile.close() - - -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.messages.frontend import extract_messages - - setup( - ... - cmdclass = {'extract_messages': extract_messages} - ) - - :see: `Integrating new distutils commands `_ - :see: `setuptools `_ - """ - - description = 'extract localizable strings from the project code' - user_options = [ - ('charset=', None, - 'charset to use in the output file'), - ('keywords=', 'k', - 'space-separated list of keywords to look for in addition to the ' - 'defaults'), - ('no-default-keywords', None, - 'do not include the default keywords'), - ('mapping-file=', 'F', - 'path to the mapping configuration file'), - ('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=', 'o', - 'name of the output file'), - ('width=', 'w', - 'set output line width (default 76)'), - ('no-wrap', None, - 'do not break long message lines, longer than the output line width, ' - 'into several lines'), - ('sort-output', None, - 'generate sorted output (default False)'), - ('sort-by-file', None, - 'sort output by file location (default False)'), - ('msgid-bugs-address=', None, - 'set report address for msgid'), - ('copyright-holder=', None, - 'set copyright holder in output'), - ('add-comments=', 'c', - 'place comment block with TAG (or those preceding keyword lines) in ' - 'output file. Seperate multiple TAGs with commas(,)'), - ('strip-comments', None, - 'strip the comment TAGs from the comments.'), - ('input-dirs=', None, - 'directories that should be scanned for messages'), - ] - boolean_options = [ - 'no-default-keywords', 'no-location', 'omit-header', 'no-wrap', - 'sort-output', 'sort-by-file', 'strip-comments' - ] - - def initialize_options(self): - self.charset = 'utf-8' - self.keywords = '' - self._keywords = DEFAULT_KEYWORDS.copy() - self.no_default_keywords = False - self.mapping_file = None - self.no_location = False - self.omit_header = False - self.output_file = None - self.input_dirs = None - self.width = None - self.no_wrap = False - self.sort_output = False - self.sort_by_file = False - self.msgid_bugs_address = None - self.copyright_holder = None - self.add_comments = None - self._add_comments = [] - self.strip_comments = False - - def finalize_options(self): - if self.no_default_keywords and not self.keywords: - raise DistutilsOptionError('you must specify new keywords if you ' - 'disable the default ones') - if self.no_default_keywords: - self._keywords = {} - if self.keywords: - self._keywords.update(parse_keywords(self.keywords.split())) - - if not self.output_file: - raise DistutilsOptionError('no output file specified') - if self.no_wrap and self.width: - raise DistutilsOptionError("'--no-wrap' and '--width' are mutually " - "exclusive") - if not self.no_wrap and not self.width: - self.width = 76 - elif self.width is not None: - self.width = int(self.width) - - if self.sort_output and self.sort_by_file: - raise DistutilsOptionError("'--sort-output' and '--sort-by-file' " - "are mutually exclusive") - - if not self.input_dirs: - self.input_dirs = dict.fromkeys([k.split('.',1)[0] - for k in self.distribution.packages - ]).keys() - - if self.add_comments: - self._add_comments = self.add_comments.split(',') - - def run(self): - mappings = self._get_mappings() - outfile = open(self.output_file, 'w') - try: - catalog = Catalog(project=self.distribution.get_name(), - version=self.distribution.get_version(), - msgid_bugs_address=self.msgid_bugs_address, - copyright_holder=self.copyright_holder, - charset=self.charset) - - for dirname, (method_map, options_map) in mappings.items(): - def callback(filename, method, options): - if method == 'ignore': - return - filepath = os.path.normpath(os.path.join(dirname, filename)) - optstr = '' - if options: - optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for - k, v in options.items()]) - log.info('extracting messages from %s%s', filepath, optstr) - - extracted = extract_from_dir(dirname, method_map, options_map, - keywords=self._keywords, - comment_tags=self._add_comments, - callback=callback, - strip_comment_tags= - self.strip_comments) - for filename, lineno, message, comments in extracted: - filepath = os.path.normpath(os.path.join(dirname, filename)) - catalog.add(message, None, [(filepath, lineno)], - auto_comments=comments) - - log.info('writing PO template file to %s' % self.output_file) - write_po(outfile, catalog, width=self.width, - no_location=self.no_location, - omit_header=self.omit_header, - sort_output=self.sort_output, - sort_by_file=self.sort_by_file) - finally: - outfile.close() - - def _get_mappings(self): - mappings = {} - - if self.mapping_file: - fileobj = open(self.mapping_file, 'U') - try: - method_map, options_map = parse_mapping(fileobj) - for dirname in self.input_dirs: - mappings[dirname] = method_map, options_map - finally: - fileobj.close() - - elif getattr(self.distribution, 'message_extractors', None): - message_extractors = self.distribution.message_extractors - for dirname, mapping in message_extractors.items(): - if isinstance(mapping, basestring): - method_map, options_map = parse_mapping(StringIO(mapping)) - else: - method_map, options_map = [], {} - for pattern, method, options in mapping: - method_map.append((pattern, method)) - options_map[pattern] = options or {} - mappings[dirname] = method_map, options_map - - else: - for dirname in self.input_dirs: - mappings[dirname] = DEFAULT_MAPPING, {} - - return mappings - - -def check_message_extractors(dist, name, value): - """Validate the ``message_extractors`` keyword argument to ``setup()``. - - :param dist: the distutils/setuptools ``Distribution`` object - :param name: the name of the keyword argument (should always be - "message_extractors") - :param value: the value of the keyword argument - :raise `DistutilsSetupError`: if the value is not valid - :see: `Adding setup() arguments - `_ - """ - assert name == 'message_extractors' - if not isinstance(value, dict): - raise DistutilsSetupError('the value of the "message_extractors" ' - 'parameter must be a dictionary') - - -class init_catalog(Command): - """New catalog initialization 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.messages.frontend import init_catalog - - setup( - ... - cmdclass = {'init_catalog': init_catalog} - ) - - :see: `Integrating new distutils commands `_ - :see: `setuptools `_ - """ - - description = 'create a new catalog based on a POT file' - user_options = [ - ('domain=', 'D', - "domain of PO file (default 'messages')"), - ('input-file=', 'i', - 'name of the input file'), - ('output-dir=', 'd', - 'path to output directory'), - ('output-file=', 'o', - "name of the output file (default " - "'//LC_MESSAGES/.po')"), - ('locale=', 'l', - 'locale for the new localized catalog'), - ] - - def initialize_options(self): - self.output_dir = None - self.output_file = None - self.input_file = None - self.locale = None - self.domain = 'messages' - - def finalize_options(self): - if not self.input_file: - raise DistutilsOptionError('you must specify the input file') - - if not self.locale: - raise DistutilsOptionError('you must provide a locale for the ' - 'new catalog') - try: - self._locale = Locale.parse(self.locale) - except UnknownLocaleError, e: - raise DistutilsOptionError(e) - - if not self.output_file and not self.output_dir: - raise DistutilsOptionError('you must specify the output directory') - if not self.output_file: - self.output_file = os.path.join(self.output_dir, self.locale, - 'LC_MESSAGES', self.domain + '.po') - - if not os.path.exists(os.path.dirname(self.output_file)): - os.makedirs(os.path.dirname(self.output_file)) - - def run(self): - log.info('creating catalog %r based on %r', self.output_file, - self.input_file) - - infile = open(self.input_file, 'r') - try: - # Although reading from the catalog template, read_po must be fed - # the locale in order to correcly calculate plurals - catalog = read_po(infile, locale=self.locale) - finally: - infile.close() - - catalog.locale = self._locale - catalog.fuzzy = False - - outfile = open(self.output_file, 'w') - try: - write_po(outfile, catalog) - finally: - outfile.close() - - -class update_catalog(Command): - """Catalog merging 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.messages.frontend import update_catalog - - setup( - ... - cmdclass = {'update_catalog': update_catalog} - ) - - :since: version 0.9 - :see: `Integrating new distutils commands `_ - :see: `setuptools `_ - """ - - description = 'update message catalogs from a POT file' - user_options = [ - ('domain=', 'D', - "domain of PO file (default 'messages')"), - ('input-file=', 'i', - 'name of the input file'), - ('output-dir=', 'd', - 'path to base directory containing the catalogs'), - ('output-file=', 'o', - "name of the output file (default " - "'//LC_MESSAGES/.po')"), - ('locale=', 'l', - 'locale of the catalog to compile'), - ('ignore-obsolete=', None, - 'whether to omit obsolete messages from the output'), - ('no-fuzzy-matching', 'N', - 'do not use fuzzy matching'), - ('previous', None, - 'keep previous msgids of translated messages') - ] - boolean_options = ['ignore_obsolete', 'no_fuzzy_matching', 'previous'] - - def initialize_options(self): - self.domain = 'messages' - self.input_file = None - self.output_dir = None - self.output_file = None - self.locale = None - self.ignore_obsolete = False - self.no_fuzzy_matching = False - self.previous = False - - def finalize_options(self): - if not self.input_file: - raise DistutilsOptionError('you must specify the input file') - if not self.output_file and not self.output_dir: - raise DistutilsOptionError('you must specify the output file or ' - 'directory') - if self.output_file and not self.locale: - raise DistutilsOptionError('you must specify the locale') - if self.no_fuzzy_matching and self.previous: - self.previous = False - - def run(self): - po_files = [] - if not self.output_file: - if self.locale: - po_files.append((self.locale, - os.path.join(self.output_dir, self.locale, - 'LC_MESSAGES', - self.domain + '.po'))) - else: - for locale in os.listdir(self.output_dir): - po_file = os.path.join(self.output_dir, locale, - 'LC_MESSAGES', - self.domain + '.po') - if os.path.exists(po_file): - po_files.append((locale, po_file)) - else: - po_files.append((self.locale, self.output_file)) - - domain = self.domain - if not domain: - domain = os.path.splitext(os.path.basename(self.input_file))[0] - - infile = open(self.input_file, 'U') - try: - template = read_po(infile) - finally: - infile.close() - - if not po_files: - raise DistutilsOptionError('no message catalogs found') - - for locale, filename in po_files: - log.info('updating catalog %r based on %r', filename, - self.input_file) - infile = open(filename, 'U') - try: - catalog = read_po(infile, locale=locale, domain=domain) - finally: - infile.close() - - catalog.update(template, self.no_fuzzy_matching) - - tmpname = os.path.join(os.path.dirname(filename), - tempfile.gettempprefix() + - os.path.basename(filename)) - tmpfile = open(tmpname, 'w') - try: - try: - write_po(tmpfile, catalog, - ignore_obsolete=self.ignore_obsolete, - include_previous=self.previous) - finally: - tmpfile.close() - except: - os.remove(tmpname) - raise - - try: - os.rename(tmpname, filename) - except OSError: - # We're probably on Windows, which doesn't support atomic - # renames, at least not through Python - # If the error is in fact due to a permissions problem, that - # same error is going to be raised from one of the following - # operations - os.remove(filename) - shutil.copy(tmpname, filename) - os.remove(tmpname) - - -class CommandLineInterface(object): - """Command-line interface. - - This class provides a simple command-line interface to the message - extraction and PO file generation functionality. - """ - - usage = '%%prog %s [options] %s' - version = '%%prog %s' % VERSION - commands = { - 'compile': 'compile message catalogs to MO files', - 'extract': 'extract messages from source files and generate a POT file', - 'init': 'create new message catalogs from a POT file', - 'update': 'update existing message catalogs from a POT file' - } - - def run(self, argv=sys.argv): - """Main entry point of the command-line interface. - - :param argv: list of arguments passed on the command-line - """ - self.parser = OptionParser(usage=self.usage % ('command', '[args]'), - version=self.version) - self.parser.disable_interspersed_args() - self.parser.print_help = self._help - self.parser.add_option('--list-locales', dest='list_locales', - action='store_true', - help="print all known locales and exit") - self.parser.add_option('-v', '--verbose', action='store_const', - dest='loglevel', const=logging.DEBUG, - help='print as much as possible') - self.parser.add_option('-q', '--quiet', action='store_const', - dest='loglevel', const=logging.ERROR, - help='print as little as possible') - self.parser.set_defaults(list_locales=False, loglevel=logging.INFO) - - options, args = self.parser.parse_args(argv[1:]) - - self._configure_logging(options.loglevel) - if options.list_locales: - identifiers = localedata.list() - longest = max([len(identifier) for identifier in identifiers]) - identifiers.sort() - format = u'%%-%ds %%s' % (longest + 1) - for identifier in identifiers: - locale = Locale.parse(identifier) - output = format % (identifier, locale.english_name) - print output.encode(sys.stdout.encoding or - getpreferredencoding() or - 'ascii', 'replace') - return 0 - - if not args: - self.parser.error('no valid command or option passed. ' - 'Try the -h/--help option for more information.') - - cmdname = args[0] - if cmdname not in self.commands: - self.parser.error('unknown command "%s"' % cmdname) - - return getattr(self, cmdname)(args[1:]) - - def _configure_logging(self, loglevel): - self.log = logging.getLogger('babel') - self.log.setLevel(loglevel) - # Don't add a new handler for every instance initialization (#227), this - # would cause duplicated output when the CommandLineInterface as an - # normal Python class. - if self.log.handlers: - handler = self.log.handlers[0] - else: - handler = logging.StreamHandler() - self.log.addHandler(handler) - handler.setLevel(loglevel) - formatter = logging.Formatter('%(message)s') - handler.setFormatter(formatter) - - def _help(self): - print self.parser.format_help() - print "commands:" - longest = max([len(command) for command in self.commands]) - format = " %%-%ds %%s" % max(8, longest + 1) - commands = self.commands.items() - commands.sort() - for name, description in commands: - print format % (name, description) - - def compile(self, argv): - """Subcommand for compiling a message catalog to a MO file. - - :param argv: the command arguments - :since: version 0.9 - """ - parser = OptionParser(usage=self.usage % ('compile', ''), - description=self.commands['compile']) - parser.add_option('--domain', '-D', dest='domain', - help="domain of MO and PO files (default '%default')") - parser.add_option('--directory', '-d', dest='directory', - metavar='DIR', help='base directory of catalog files') - parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', - help='locale of the catalog') - parser.add_option('--input-file', '-i', dest='input_file', - metavar='FILE', help='name of the input file') - parser.add_option('--output-file', '-o', dest='output_file', - metavar='FILE', - help="name of the output file (default " - "'//LC_MESSAGES/" - ".mo')") - parser.add_option('--use-fuzzy', '-f', dest='use_fuzzy', - action='store_true', - help='also include fuzzy translations (default ' - '%default)') - parser.add_option('--statistics', dest='statistics', - action='store_true', - help='print statistics about translations') - - parser.set_defaults(domain='messages', use_fuzzy=False, - compile_all=False, statistics=False) - options, args = parser.parse_args(argv) - - po_files = [] - mo_files = [] - if not options.input_file: - if not options.directory: - parser.error('you must specify either the input file or the ' - 'base directory') - if options.locale: - po_files.append((options.locale, - os.path.join(options.directory, - options.locale, 'LC_MESSAGES', - options.domain + '.po'))) - mo_files.append(os.path.join(options.directory, options.locale, - 'LC_MESSAGES', - options.domain + '.mo')) - else: - for locale in os.listdir(options.directory): - po_file = os.path.join(options.directory, locale, - 'LC_MESSAGES', options.domain + '.po') - if os.path.exists(po_file): - po_files.append((locale, po_file)) - mo_files.append(os.path.join(options.directory, locale, - 'LC_MESSAGES', - options.domain + '.mo')) - else: - po_files.append((options.locale, options.input_file)) - if options.output_file: - mo_files.append(options.output_file) - else: - if not options.directory: - parser.error('you must specify either the input file or ' - 'the base directory') - mo_files.append(os.path.join(options.directory, options.locale, - 'LC_MESSAGES', - options.domain + '.mo')) - if not po_files: - parser.error('no message catalogs found') - - for idx, (locale, po_file) in enumerate(po_files): - mo_file = mo_files[idx] - infile = open(po_file, 'r') - try: - catalog = read_po(infile, locale) - finally: - infile.close() - - if options.statistics: - translated = 0 - for message in list(catalog)[1:]: - if message.string: - translated +=1 - percentage = 0 - if len(catalog): - percentage = translated * 100 // len(catalog) - self.log.info("%d of %d messages (%d%%) translated in %r", - translated, len(catalog), percentage, po_file) - - if catalog.fuzzy and not options.use_fuzzy: - self.log.warn('catalog %r is marked as fuzzy, skipping', - po_file) - continue - - for message, errors in catalog.check(): - for error in errors: - self.log.error('error: %s:%d: %s', po_file, message.lineno, - error) - - self.log.info('compiling catalog %r to %r', po_file, mo_file) - - outfile = open(mo_file, 'wb') - try: - write_mo(outfile, catalog, use_fuzzy=options.use_fuzzy) - finally: - outfile.close() - - def extract(self, argv): - """Subcommand for extracting messages from source files and generating - a POT file. - - :param argv: the command arguments - """ - parser = OptionParser(usage=self.usage % ('extract', 'dir1 ...'), - description=self.commands['extract']) - parser.add_option('--charset', dest='charset', - help='charset to use in the output (default ' - '"%default")') - parser.add_option('-k', '--keyword', dest='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-default-keywords', dest='no_default_keywords', - action='store_true', - help="do not include the default keywords") - parser.add_option('--mapping', '-F', dest='mapping_file', - help='path to the extraction mapping file') - parser.add_option('--no-location', dest='no_location', - action='store_true', - help='do not include location comments with filename ' - 'and line number') - parser.add_option('--omit-header', dest='omit_header', - 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') - parser.add_option('-w', '--width', dest='width', type='int', - help="set output line width (default 76)") - parser.add_option('--no-wrap', dest='no_wrap', action = 'store_true', - help='do not break long message lines, longer than ' - 'the output line width, into several lines') - parser.add_option('--sort-output', dest='sort_output', - action='store_true', - help='generate sorted output (default False)') - parser.add_option('--sort-by-file', dest='sort_by_file', - action='store_true', - help='sort output by file location (default False)') - parser.add_option('--msgid-bugs-address', dest='msgid_bugs_address', - metavar='EMAIL@ADDRESS', - help='set report address for msgid') - parser.add_option('--copyright-holder', dest='copyright_holder', - help='set copyright holder in output') - parser.add_option('--project', dest='project', - help='set project name in output') - parser.add_option('--version', dest='version', - help='set project version in output') - parser.add_option('--add-comments', '-c', dest='comment_tags', - metavar='TAG', action='append', - help='place comment block with TAG (or those ' - 'preceding keyword lines) in output file. One ' - 'TAG per argument call') - parser.add_option('--strip-comment-tags', '-s', - dest='strip_comment_tags', action='store_true', - help='Strip the comment tags from the comments.') - - parser.set_defaults(charset='utf-8', keywords=[], - no_default_keywords=False, no_location=False, - omit_header = False, width=None, no_wrap=False, - sort_output=False, sort_by_file=False, - comment_tags=[], strip_comment_tags=False) - options, args = parser.parse_args(argv) - if not args: - parser.error('incorrect number of arguments') - - if options.output not in (None, '-'): - outfile = open(options.output, 'w') - else: - outfile = sys.stdout - - keywords = DEFAULT_KEYWORDS.copy() - if options.no_default_keywords: - if not options.keywords: - parser.error('you must specify new keywords if you disable the ' - 'default ones') - keywords = {} - if options.keywords: - keywords.update(parse_keywords(options.keywords)) - - if options.mapping_file: - fileobj = open(options.mapping_file, 'U') - try: - method_map, options_map = parse_mapping(fileobj) - finally: - fileobj.close() - else: - method_map = DEFAULT_MAPPING - options_map = {} - - if options.width and options.no_wrap: - parser.error("'--no-wrap' and '--width' are mutually exclusive.") - elif not options.width and not options.no_wrap: - options.width = 76 - - if options.sort_output and options.sort_by_file: - parser.error("'--sort-output' and '--sort-by-file' are mutually " - "exclusive") - - try: - catalog = Catalog(project=options.project, - version=options.version, - msgid_bugs_address=options.msgid_bugs_address, - copyright_holder=options.copyright_holder, - charset=options.charset) - - for dirname in args: - if not os.path.isdir(dirname): - parser.error('%r is not a directory' % dirname) - - def callback(filename, method, options): - if method == 'ignore': - return - filepath = os.path.normpath(os.path.join(dirname, filename)) - optstr = '' - if options: - optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for - k, v in options.items()]) - self.log.info('extracting messages from %s%s', filepath, - optstr) - - extracted = extract_from_dir(dirname, method_map, options_map, - keywords, options.comment_tags, - callback=callback, - strip_comment_tags= - options.strip_comment_tags) - for filename, lineno, message, comments in extracted: - filepath = os.path.normpath(os.path.join(dirname, filename)) - catalog.add(message, None, [(filepath, lineno)], - auto_comments=comments) - - if options.output not in (None, '-'): - self.log.info('writing PO template file to %s' % options.output) - write_po(outfile, catalog, width=options.width, - no_location=options.no_location, - omit_header=options.omit_header, - sort_output=options.sort_output, - sort_by_file=options.sort_by_file) - finally: - if options.output: - outfile.close() - - def init(self, argv): - """Subcommand for creating new message catalogs from a template. - - :param argv: the command arguments - """ - parser = OptionParser(usage=self.usage % ('init', ''), - description=self.commands['init']) - parser.add_option('--domain', '-D', dest='domain', - help="domain of PO file (default '%default')") - parser.add_option('--input-file', '-i', dest='input_file', - metavar='FILE', help='name of the input file') - parser.add_option('--output-dir', '-d', dest='output_dir', - metavar='DIR', help='path to output directory') - parser.add_option('--output-file', '-o', dest='output_file', - metavar='FILE', - help="name of the output file (default " - "'//LC_MESSAGES/" - ".po')") - parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', - help='locale for the new localized catalog') - - parser.set_defaults(domain='messages') - options, args = parser.parse_args(argv) - - if not options.locale: - parser.error('you must provide a locale for the new catalog') - try: - locale = Locale.parse(options.locale) - except UnknownLocaleError, e: - parser.error(e) - - if not options.input_file: - parser.error('you must specify the input file') - - if not options.output_file and not options.output_dir: - parser.error('you must specify the output file or directory') - - if not options.output_file: - options.output_file = os.path.join(options.output_dir, - options.locale, 'LC_MESSAGES', - options.domain + '.po') - if not os.path.exists(os.path.dirname(options.output_file)): - os.makedirs(os.path.dirname(options.output_file)) - - infile = open(options.input_file, 'r') - try: - # Although reading from the catalog template, read_po must be fed - # the locale in order to correcly calculate plurals - catalog = read_po(infile, locale=options.locale) - finally: - infile.close() - - catalog.locale = locale - catalog.revision_date = datetime.now(LOCALTZ) - - self.log.info('creating catalog %r based on %r', options.output_file, - options.input_file) - - outfile = open(options.output_file, 'w') - try: - write_po(outfile, catalog) - finally: - outfile.close() - - def update(self, argv): - """Subcommand for updating existing message catalogs from a template. - - :param argv: the command arguments - :since: version 0.9 - """ - parser = OptionParser(usage=self.usage % ('update', ''), - description=self.commands['update']) - parser.add_option('--domain', '-D', dest='domain', - help="domain of PO file (default '%default')") - parser.add_option('--input-file', '-i', dest='input_file', - metavar='FILE', help='name of the input file') - parser.add_option('--output-dir', '-d', dest='output_dir', - metavar='DIR', help='path to output directory') - parser.add_option('--output-file', '-o', dest='output_file', - metavar='FILE', - help="name of the output file (default " - "'//LC_MESSAGES/" - ".po')") - parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', - help='locale of the translations catalog') - parser.add_option('--ignore-obsolete', dest='ignore_obsolete', - action='store_true', - help='do not include obsolete messages in the output ' - '(default %default)') - parser.add_option('--no-fuzzy-matching', '-N', dest='no_fuzzy_matching', - action='store_true', - help='do not use fuzzy matching (default %default)') - parser.add_option('--previous', dest='previous', action='store_true', - help='keep previous msgids of translated messages ' - '(default %default)') - - parser.set_defaults(domain='messages', ignore_obsolete=False, - no_fuzzy_matching=False, previous=False) - options, args = parser.parse_args(argv) - - if not options.input_file: - parser.error('you must specify the input file') - if not options.output_file and not options.output_dir: - parser.error('you must specify the output file or directory') - if options.output_file and not options.locale: - parser.error('you must specify the locale') - if options.no_fuzzy_matching and options.previous: - options.previous = False - - po_files = [] - if not options.output_file: - if options.locale: - po_files.append((options.locale, - os.path.join(options.output_dir, - options.locale, 'LC_MESSAGES', - options.domain + '.po'))) - else: - for locale in os.listdir(options.output_dir): - po_file = os.path.join(options.output_dir, locale, - 'LC_MESSAGES', - options.domain + '.po') - if os.path.exists(po_file): - po_files.append((locale, po_file)) - else: - po_files.append((options.locale, options.output_file)) - - domain = options.domain - if not domain: - domain = os.path.splitext(os.path.basename(options.input_file))[0] - - infile = open(options.input_file, 'U') - try: - template = read_po(infile) - finally: - infile.close() - - if not po_files: - parser.error('no message catalogs found') - - for locale, filename in po_files: - self.log.info('updating catalog %r based on %r', filename, - options.input_file) - infile = open(filename, 'U') - try: - catalog = read_po(infile, locale=locale, domain=domain) - finally: - infile.close() - - catalog.update(template, options.no_fuzzy_matching) - - tmpname = os.path.join(os.path.dirname(filename), - tempfile.gettempprefix() + - os.path.basename(filename)) - tmpfile = open(tmpname, 'w') - try: - try: - write_po(tmpfile, catalog, - ignore_obsolete=options.ignore_obsolete, - include_previous=options.previous) - finally: - tmpfile.close() - except: - os.remove(tmpname) - raise - - try: - os.rename(tmpname, filename) - except OSError: - # We're probably on Windows, which doesn't support atomic - # renames, at least not through Python - # If the error is in fact due to a permissions problem, that - # same error is going to be raised from one of the following - # operations - os.remove(filename) - shutil.copy(tmpname, filename) - os.remove(tmpname) - - -def main(): - return CommandLineInterface().run(sys.argv) - -def parse_mapping(fileobj, filename=None): - """Parse an extraction method mapping from a file-like object. - - >>> buf = StringIO(''' - ... [extractors] - ... custom = mypackage.module:myfunc - ... - ... # Python source files - ... [python: **.py] - ... - ... # Genshi templates - ... [genshi: **/templates/**.html] - ... include_attrs = - ... [genshi: **/templates/**.txt] - ... template_class = genshi.template:TextTemplate - ... encoding = latin-1 - ... - ... # Some custom extractor - ... [custom: **/custom/*.*] - ... ''') - - >>> method_map, options_map = parse_mapping(buf) - >>> len(method_map) - 4 - - >>> method_map[0] - ('**.py', 'python') - >>> options_map['**.py'] - {} - >>> method_map[1] - ('**/templates/**.html', 'genshi') - >>> options_map['**/templates/**.html']['include_attrs'] - '' - >>> method_map[2] - ('**/templates/**.txt', 'genshi') - >>> options_map['**/templates/**.txt']['template_class'] - 'genshi.template:TextTemplate' - >>> options_map['**/templates/**.txt']['encoding'] - 'latin-1' - - >>> method_map[3] - ('**/custom/*.*', 'mypackage.module:myfunc') - >>> options_map['**/custom/*.*'] - {} - - :param fileobj: a readable file-like object containing the configuration - text to parse - :return: a `(method_map, options_map)` tuple - :rtype: `tuple` - :see: `extract_from_directory` - """ - extractors = {} - method_map = [] - options_map = {} - - parser = RawConfigParser() - parser._sections = odict(parser._sections) # We need ordered sections - parser.readfp(fileobj, filename) - for section in parser.sections(): - if section == 'extractors': - extractors = dict(parser.items(section)) - else: - method, pattern = [part.strip() for part in section.split(':', 1)] - method_map.append((pattern, method)) - options_map[pattern] = dict(parser.items(section)) - - if extractors: - for idx, (pattern, method) in enumerate(method_map): - if method in extractors: - method = extractors[method] - method_map[idx] = (pattern, method) - - return (method_map, options_map) - -def parse_keywords(strings=[]): - """Parse keywords specifications from the given list of strings. - - >>> kw = parse_keywords(['_', 'dgettext:2', 'dngettext:2,3']).items() - >>> kw.sort() - >>> for keyword, indices in kw: - ... print (keyword, indices) - ('_', None) - ('dgettext', (2,)) - ('dngettext', (2, 3)) - """ - keywords = {} - for string in strings: - if ':' in string: - funcname, indices = string.split(':') - else: - funcname, indices = string, None - if funcname not in keywords: - if indices: - indices = tuple([(int(x)) for x in indices.split(',')]) - keywords[funcname] = indices - return keywords - - -if __name__ == '__main__': - main() +#!/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/. + +"""Frontends for the message extraction functionality.""" + +from ConfigParser import RawConfigParser +from datetime import datetime +from distutils import log +from distutils.cmd import Command +from distutils.errors import DistutilsOptionError, DistutilsSetupError +from locale import getpreferredencoding +import logging +from optparse import OptionParser +import os +import re +import shutil +from StringIO import StringIO +import sys +import tempfile + +from babel import __version__ as VERSION +from babel import Locale, localedata +from babel.core import UnknownLocaleError +from babel.messages.catalog import Catalog +from babel.messages.extract import extract_from_dir, DEFAULT_KEYWORDS, \ + DEFAULT_MAPPING +from babel.messages.mofile import write_mo +from babel.messages.pofile import read_po, write_po +from babel.messages.plurals import PLURALS +from babel.util import odict, LOCALTZ + +__all__ = ['CommandLineInterface', 'compile_catalog', 'extract_messages', + 'init_catalog', 'check_message_extractors', 'update_catalog'] +__docformat__ = 'restructuredtext en' + + +class compile_catalog(Command): + """Catalog compilation 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.messages.frontend import compile_catalog + + setup( + ... + cmdclass = {'compile_catalog': compile_catalog} + ) + + :since: version 0.9 + :see: `Integrating new distutils commands `_ + :see: `setuptools `_ + """ + + description = 'compile message catalogs to binary MO files' + user_options = [ + ('domain=', 'D', + "domain of PO file (default 'messages')"), + ('directory=', 'd', + 'path to base directory containing the catalogs'), + ('input-file=', 'i', + 'name of the input file'), + ('output-file=', 'o', + "name of the output file (default " + "'//LC_MESSAGES/.po')"), + ('locale=', 'l', + 'locale of the catalog to compile'), + ('use-fuzzy', 'f', + 'also include fuzzy translations'), + ('statistics', None, + 'print statistics about translations') + ] + boolean_options = ['use-fuzzy', 'statistics'] + + def initialize_options(self): + self.domain = 'messages' + self.directory = None + self.input_file = None + self.output_file = None + self.locale = None + self.use_fuzzy = False + self.statistics = False + + def finalize_options(self): + if not self.input_file and not self.directory: + raise DistutilsOptionError('you must specify either the input file ' + 'or the base directory') + if not self.output_file and not self.directory: + raise DistutilsOptionError('you must specify either the input file ' + 'or the base directory') + + def run(self): + po_files = [] + mo_files = [] + + if not self.input_file: + if self.locale: + po_files.append((self.locale, + os.path.join(self.directory, self.locale, + 'LC_MESSAGES', + self.domain + '.po'))) + mo_files.append(os.path.join(self.directory, self.locale, + 'LC_MESSAGES', + self.domain + '.mo')) + else: + for locale in os.listdir(self.directory): + po_file = os.path.join(self.directory, locale, + 'LC_MESSAGES', self.domain + '.po') + if os.path.exists(po_file): + po_files.append((locale, po_file)) + mo_files.append(os.path.join(self.directory, locale, + 'LC_MESSAGES', + self.domain + '.mo')) + else: + po_files.append((self.locale, self.input_file)) + if self.output_file: + mo_files.append(self.output_file) + else: + mo_files.append(os.path.join(self.directory, self.locale, + 'LC_MESSAGES', + self.domain + '.mo')) + + if not po_files: + raise DistutilsOptionError('no message catalogs found') + + for idx, (locale, po_file) in enumerate(po_files): + mo_file = mo_files[idx] + infile = open(po_file, 'r') + try: + catalog = read_po(infile, locale) + finally: + infile.close() + + if self.statistics: + translated = 0 + for message in list(catalog)[1:]: + if message.string: + translated +=1 + percentage = 0 + if len(catalog): + percentage = translated * 100 // len(catalog) + log.info('%d of %d messages (%d%%) translated in %r', + translated, len(catalog), percentage, po_file) + + if catalog.fuzzy and not self.use_fuzzy: + log.warn('catalog %r is marked as fuzzy, skipping', po_file) + continue + + for message, errors in catalog.check(): + for error in errors: + log.error('error: %s:%d: %s', po_file, message.lineno, + error) + + log.info('compiling catalog %r to %r', po_file, mo_file) + + outfile = open(mo_file, 'wb') + try: + write_mo(outfile, catalog, use_fuzzy=self.use_fuzzy) + finally: + outfile.close() + + +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.messages.frontend import extract_messages + + setup( + ... + cmdclass = {'extract_messages': extract_messages} + ) + + :see: `Integrating new distutils commands `_ + :see: `setuptools `_ + """ + + description = 'extract localizable strings from the project code' + user_options = [ + ('charset=', None, + 'charset to use in the output file'), + ('keywords=', 'k', + 'space-separated list of keywords to look for in addition to the ' + 'defaults'), + ('no-default-keywords', None, + 'do not include the default keywords'), + ('mapping-file=', 'F', + 'path to the mapping configuration file'), + ('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=', 'o', + 'name of the output file'), + ('width=', 'w', + 'set output line width (default 76)'), + ('no-wrap', None, + 'do not break long message lines, longer than the output line width, ' + 'into several lines'), + ('sort-output', None, + 'generate sorted output (default False)'), + ('sort-by-file', None, + 'sort output by file location (default False)'), + ('msgid-bugs-address=', None, + 'set report address for msgid'), + ('copyright-holder=', None, + 'set copyright holder in output'), + ('add-comments=', 'c', + 'place comment block with TAG (or those preceding keyword lines) in ' + 'output file. Seperate multiple TAGs with commas(,)'), + ('strip-comments', None, + 'strip the comment TAGs from the comments.'), + ('input-dirs=', None, + 'directories that should be scanned for messages'), + ] + boolean_options = [ + 'no-default-keywords', 'no-location', 'omit-header', 'no-wrap', + 'sort-output', 'sort-by-file', 'strip-comments' + ] + + def initialize_options(self): + self.charset = 'utf-8' + self.keywords = '' + self._keywords = DEFAULT_KEYWORDS.copy() + self.no_default_keywords = False + self.mapping_file = None + self.no_location = False + self.omit_header = False + self.output_file = None + self.input_dirs = None + self.width = None + self.no_wrap = False + self.sort_output = False + self.sort_by_file = False + self.msgid_bugs_address = None + self.copyright_holder = None + self.add_comments = None + self._add_comments = [] + self.strip_comments = False + + def finalize_options(self): + if self.no_default_keywords and not self.keywords: + raise DistutilsOptionError('you must specify new keywords if you ' + 'disable the default ones') + if self.no_default_keywords: + self._keywords = {} + if self.keywords: + self._keywords.update(parse_keywords(self.keywords.split())) + + if not self.output_file: + raise DistutilsOptionError('no output file specified') + if self.no_wrap and self.width: + raise DistutilsOptionError("'--no-wrap' and '--width' are mutually " + "exclusive") + if not self.no_wrap and not self.width: + self.width = 76 + elif self.width is not None: + self.width = int(self.width) + + if self.sort_output and self.sort_by_file: + raise DistutilsOptionError("'--sort-output' and '--sort-by-file' " + "are mutually exclusive") + + if not self.input_dirs: + self.input_dirs = dict.fromkeys([k.split('.',1)[0] + for k in self.distribution.packages + ]).keys() + + if self.add_comments: + self._add_comments = self.add_comments.split(',') + + def run(self): + mappings = self._get_mappings() + outfile = open(self.output_file, 'w') + try: + catalog = Catalog(project=self.distribution.get_name(), + version=self.distribution.get_version(), + msgid_bugs_address=self.msgid_bugs_address, + copyright_holder=self.copyright_holder, + charset=self.charset) + + for dirname, (method_map, options_map) in mappings.items(): + def callback(filename, method, options): + if method == 'ignore': + return + filepath = os.path.normpath(os.path.join(dirname, filename)) + optstr = '' + if options: + optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for + k, v in options.items()]) + log.info('extracting messages from %s%s', filepath, optstr) + + extracted = extract_from_dir(dirname, method_map, options_map, + keywords=self._keywords, + comment_tags=self._add_comments, + callback=callback, + strip_comment_tags= + self.strip_comments) + for filename, lineno, message, comments in extracted: + filepath = os.path.normpath(os.path.join(dirname, filename)) + catalog.add(message, None, [(filepath, lineno)], + auto_comments=comments) + + log.info('writing PO template file to %s' % self.output_file) + write_po(outfile, catalog, width=self.width, + no_location=self.no_location, + omit_header=self.omit_header, + sort_output=self.sort_output, + sort_by_file=self.sort_by_file) + finally: + outfile.close() + + def _get_mappings(self): + mappings = {} + + if self.mapping_file: + fileobj = open(self.mapping_file, 'U') + try: + method_map, options_map = parse_mapping(fileobj) + for dirname in self.input_dirs: + mappings[dirname] = method_map, options_map + finally: + fileobj.close() + + elif getattr(self.distribution, 'message_extractors', None): + message_extractors = self.distribution.message_extractors + for dirname, mapping in message_extractors.items(): + if isinstance(mapping, basestring): + method_map, options_map = parse_mapping(StringIO(mapping)) + else: + method_map, options_map = [], {} + for pattern, method, options in mapping: + method_map.append((pattern, method)) + options_map[pattern] = options or {} + mappings[dirname] = method_map, options_map + + else: + for dirname in self.input_dirs: + mappings[dirname] = DEFAULT_MAPPING, {} + + return mappings + + +def check_message_extractors(dist, name, value): + """Validate the ``message_extractors`` keyword argument to ``setup()``. + + :param dist: the distutils/setuptools ``Distribution`` object + :param name: the name of the keyword argument (should always be + "message_extractors") + :param value: the value of the keyword argument + :raise `DistutilsSetupError`: if the value is not valid + :see: `Adding setup() arguments + `_ + """ + assert name == 'message_extractors' + if not isinstance(value, dict): + raise DistutilsSetupError('the value of the "message_extractors" ' + 'parameter must be a dictionary') + + +class init_catalog(Command): + """New catalog initialization 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.messages.frontend import init_catalog + + setup( + ... + cmdclass = {'init_catalog': init_catalog} + ) + + :see: `Integrating new distutils commands `_ + :see: `setuptools `_ + """ + + description = 'create a new catalog based on a POT file' + user_options = [ + ('domain=', 'D', + "domain of PO file (default 'messages')"), + ('input-file=', 'i', + 'name of the input file'), + ('output-dir=', 'd', + 'path to output directory'), + ('output-file=', 'o', + "name of the output file (default " + "'//LC_MESSAGES/.po')"), + ('locale=', 'l', + 'locale for the new localized catalog'), + ] + + def initialize_options(self): + self.output_dir = None + self.output_file = None + self.input_file = None + self.locale = None + self.domain = 'messages' + + def finalize_options(self): + if not self.input_file: + raise DistutilsOptionError('you must specify the input file') + + if not self.locale: + raise DistutilsOptionError('you must provide a locale for the ' + 'new catalog') + try: + self._locale = Locale.parse(self.locale) + except UnknownLocaleError, e: + raise DistutilsOptionError(e) + + if not self.output_file and not self.output_dir: + raise DistutilsOptionError('you must specify the output directory') + if not self.output_file: + self.output_file = os.path.join(self.output_dir, self.locale, + 'LC_MESSAGES', self.domain + '.po') + + if not os.path.exists(os.path.dirname(self.output_file)): + os.makedirs(os.path.dirname(self.output_file)) + + def run(self): + log.info('creating catalog %r based on %r', self.output_file, + self.input_file) + + infile = open(self.input_file, 'r') + try: + # Although reading from the catalog template, read_po must be fed + # the locale in order to correcly calculate plurals + catalog = read_po(infile, locale=self.locale) + finally: + infile.close() + + catalog.locale = self._locale + catalog.fuzzy = False + + outfile = open(self.output_file, 'w') + try: + write_po(outfile, catalog) + finally: + outfile.close() + + +class update_catalog(Command): + """Catalog merging 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.messages.frontend import update_catalog + + setup( + ... + cmdclass = {'update_catalog': update_catalog} + ) + + :since: version 0.9 + :see: `Integrating new distutils commands `_ + :see: `setuptools `_ + """ + + description = 'update message catalogs from a POT file' + user_options = [ + ('domain=', 'D', + "domain of PO file (default 'messages')"), + ('input-file=', 'i', + 'name of the input file'), + ('output-dir=', 'd', + 'path to base directory containing the catalogs'), + ('output-file=', 'o', + "name of the output file (default " + "'//LC_MESSAGES/.po')"), + ('locale=', 'l', + 'locale of the catalog to compile'), + ('ignore-obsolete=', None, + 'whether to omit obsolete messages from the output'), + ('no-fuzzy-matching', 'N', + 'do not use fuzzy matching'), + ('previous', None, + 'keep previous msgids of translated messages') + ] + boolean_options = ['ignore_obsolete', 'no_fuzzy_matching', 'previous'] + + def initialize_options(self): + self.domain = 'messages' + self.input_file = None + self.output_dir = None + self.output_file = None + self.locale = None + self.ignore_obsolete = False + self.no_fuzzy_matching = False + self.previous = False + + def finalize_options(self): + if not self.input_file: + raise DistutilsOptionError('you must specify the input file') + if not self.output_file and not self.output_dir: + raise DistutilsOptionError('you must specify the output file or ' + 'directory') + if self.output_file and not self.locale: + raise DistutilsOptionError('you must specify the locale') + if self.no_fuzzy_matching and self.previous: + self.previous = False + + def run(self): + po_files = [] + if not self.output_file: + if self.locale: + po_files.append((self.locale, + os.path.join(self.output_dir, self.locale, + 'LC_MESSAGES', + self.domain + '.po'))) + else: + for locale in os.listdir(self.output_dir): + po_file = os.path.join(self.output_dir, locale, + 'LC_MESSAGES', + self.domain + '.po') + if os.path.exists(po_file): + po_files.append((locale, po_file)) + else: + po_files.append((self.locale, self.output_file)) + + domain = self.domain + if not domain: + domain = os.path.splitext(os.path.basename(self.input_file))[0] + + infile = open(self.input_file, 'U') + try: + template = read_po(infile) + finally: + infile.close() + + if not po_files: + raise DistutilsOptionError('no message catalogs found') + + for locale, filename in po_files: + log.info('updating catalog %r based on %r', filename, + self.input_file) + infile = open(filename, 'U') + try: + catalog = read_po(infile, locale=locale, domain=domain) + finally: + infile.close() + + catalog.update(template, self.no_fuzzy_matching) + + tmpname = os.path.join(os.path.dirname(filename), + tempfile.gettempprefix() + + os.path.basename(filename)) + tmpfile = open(tmpname, 'w') + try: + try: + write_po(tmpfile, catalog, + ignore_obsolete=self.ignore_obsolete, + include_previous=self.previous) + finally: + tmpfile.close() + except: + os.remove(tmpname) + raise + + try: + os.rename(tmpname, filename) + except OSError: + # We're probably on Windows, which doesn't support atomic + # renames, at least not through Python + # If the error is in fact due to a permissions problem, that + # same error is going to be raised from one of the following + # operations + os.remove(filename) + shutil.copy(tmpname, filename) + os.remove(tmpname) + + +class CommandLineInterface(object): + """Command-line interface. + + This class provides a simple command-line interface to the message + extraction and PO file generation functionality. + """ + + usage = '%%prog %s [options] %s' + version = '%%prog %s' % VERSION + commands = { + 'compile': 'compile message catalogs to MO files', + 'extract': 'extract messages from source files and generate a POT file', + 'init': 'create new message catalogs from a POT file', + 'update': 'update existing message catalogs from a POT file' + } + + def run(self, argv=sys.argv): + """Main entry point of the command-line interface. + + :param argv: list of arguments passed on the command-line + """ + self.parser = OptionParser(usage=self.usage % ('command', '[args]'), + version=self.version) + self.parser.disable_interspersed_args() + self.parser.print_help = self._help + self.parser.add_option('--list-locales', dest='list_locales', + action='store_true', + help="print all known locales and exit") + self.parser.add_option('-v', '--verbose', action='store_const', + dest='loglevel', const=logging.DEBUG, + help='print as much as possible') + self.parser.add_option('-q', '--quiet', action='store_const', + dest='loglevel', const=logging.ERROR, + help='print as little as possible') + self.parser.set_defaults(list_locales=False, loglevel=logging.INFO) + + options, args = self.parser.parse_args(argv[1:]) + + self._configure_logging(options.loglevel) + if options.list_locales: + identifiers = localedata.list() + longest = max([len(identifier) for identifier in identifiers]) + identifiers.sort() + format = u'%%-%ds %%s' % (longest + 1) + for identifier in identifiers: + locale = Locale.parse(identifier) + output = format % (identifier, locale.english_name) + print output.encode(sys.stdout.encoding or + getpreferredencoding() or + 'ascii', 'replace') + return 0 + + if not args: + self.parser.error('no valid command or option passed. ' + 'Try the -h/--help option for more information.') + + cmdname = args[0] + if cmdname not in self.commands: + self.parser.error('unknown command "%s"' % cmdname) + + return getattr(self, cmdname)(args[1:]) + + def _configure_logging(self, loglevel): + self.log = logging.getLogger('babel') + self.log.setLevel(loglevel) + # Don't add a new handler for every instance initialization (#227), this + # would cause duplicated output when the CommandLineInterface as an + # normal Python class. + if self.log.handlers: + handler = self.log.handlers[0] + else: + handler = logging.StreamHandler() + self.log.addHandler(handler) + handler.setLevel(loglevel) + formatter = logging.Formatter('%(message)s') + handler.setFormatter(formatter) + + def _help(self): + print self.parser.format_help() + print "commands:" + longest = max([len(command) for command in self.commands]) + format = " %%-%ds %%s" % max(8, longest + 1) + commands = self.commands.items() + commands.sort() + for name, description in commands: + print format % (name, description) + + def compile(self, argv): + """Subcommand for compiling a message catalog to a MO file. + + :param argv: the command arguments + :since: version 0.9 + """ + parser = OptionParser(usage=self.usage % ('compile', ''), + description=self.commands['compile']) + parser.add_option('--domain', '-D', dest='domain', + help="domain of MO and PO files (default '%default')") + parser.add_option('--directory', '-d', dest='directory', + metavar='DIR', help='base directory of catalog files') + parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', + help='locale of the catalog') + parser.add_option('--input-file', '-i', dest='input_file', + metavar='FILE', help='name of the input file') + parser.add_option('--output-file', '-o', dest='output_file', + metavar='FILE', + help="name of the output file (default " + "'//LC_MESSAGES/" + ".mo')") + parser.add_option('--use-fuzzy', '-f', dest='use_fuzzy', + action='store_true', + help='also include fuzzy translations (default ' + '%default)') + parser.add_option('--statistics', dest='statistics', + action='store_true', + help='print statistics about translations') + + parser.set_defaults(domain='messages', use_fuzzy=False, + compile_all=False, statistics=False) + options, args = parser.parse_args(argv) + + po_files = [] + mo_files = [] + if not options.input_file: + if not options.directory: + parser.error('you must specify either the input file or the ' + 'base directory') + if options.locale: + po_files.append((options.locale, + os.path.join(options.directory, + options.locale, 'LC_MESSAGES', + options.domain + '.po'))) + mo_files.append(os.path.join(options.directory, options.locale, + 'LC_MESSAGES', + options.domain + '.mo')) + else: + for locale in os.listdir(options.directory): + po_file = os.path.join(options.directory, locale, + 'LC_MESSAGES', options.domain + '.po') + if os.path.exists(po_file): + po_files.append((locale, po_file)) + mo_files.append(os.path.join(options.directory, locale, + 'LC_MESSAGES', + options.domain + '.mo')) + else: + po_files.append((options.locale, options.input_file)) + if options.output_file: + mo_files.append(options.output_file) + else: + if not options.directory: + parser.error('you must specify either the input file or ' + 'the base directory') + mo_files.append(os.path.join(options.directory, options.locale, + 'LC_MESSAGES', + options.domain + '.mo')) + if not po_files: + parser.error('no message catalogs found') + + for idx, (locale, po_file) in enumerate(po_files): + mo_file = mo_files[idx] + infile = open(po_file, 'r') + try: + catalog = read_po(infile, locale) + finally: + infile.close() + + if options.statistics: + translated = 0 + for message in list(catalog)[1:]: + if message.string: + translated +=1 + percentage = 0 + if len(catalog): + percentage = translated * 100 // len(catalog) + self.log.info("%d of %d messages (%d%%) translated in %r", + translated, len(catalog), percentage, po_file) + + if catalog.fuzzy and not options.use_fuzzy: + self.log.warn('catalog %r is marked as fuzzy, skipping', + po_file) + continue + + for message, errors in catalog.check(): + for error in errors: + self.log.error('error: %s:%d: %s', po_file, message.lineno, + error) + + self.log.info('compiling catalog %r to %r', po_file, mo_file) + + outfile = open(mo_file, 'wb') + try: + write_mo(outfile, catalog, use_fuzzy=options.use_fuzzy) + finally: + outfile.close() + + def extract(self, argv): + """Subcommand for extracting messages from source files and generating + a POT file. + + :param argv: the command arguments + """ + parser = OptionParser(usage=self.usage % ('extract', 'dir1 ...'), + description=self.commands['extract']) + parser.add_option('--charset', dest='charset', + help='charset to use in the output (default ' + '"%default")') + parser.add_option('-k', '--keyword', dest='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-default-keywords', dest='no_default_keywords', + action='store_true', + help="do not include the default keywords") + parser.add_option('--mapping', '-F', dest='mapping_file', + help='path to the extraction mapping file') + parser.add_option('--no-location', dest='no_location', + action='store_true', + help='do not include location comments with filename ' + 'and line number') + parser.add_option('--omit-header', dest='omit_header', + 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') + parser.add_option('-w', '--width', dest='width', type='int', + help="set output line width (default 76)") + parser.add_option('--no-wrap', dest='no_wrap', action = 'store_true', + help='do not break long message lines, longer than ' + 'the output line width, into several lines') + parser.add_option('--sort-output', dest='sort_output', + action='store_true', + help='generate sorted output (default False)') + parser.add_option('--sort-by-file', dest='sort_by_file', + action='store_true', + help='sort output by file location (default False)') + parser.add_option('--msgid-bugs-address', dest='msgid_bugs_address', + metavar='EMAIL@ADDRESS', + help='set report address for msgid') + parser.add_option('--copyright-holder', dest='copyright_holder', + help='set copyright holder in output') + parser.add_option('--project', dest='project', + help='set project name in output') + parser.add_option('--version', dest='version', + help='set project version in output') + parser.add_option('--add-comments', '-c', dest='comment_tags', + metavar='TAG', action='append', + help='place comment block with TAG (or those ' + 'preceding keyword lines) in output file. One ' + 'TAG per argument call') + parser.add_option('--strip-comment-tags', '-s', + dest='strip_comment_tags', action='store_true', + help='Strip the comment tags from the comments.') + + parser.set_defaults(charset='utf-8', keywords=[], + no_default_keywords=False, no_location=False, + omit_header = False, width=None, no_wrap=False, + sort_output=False, sort_by_file=False, + comment_tags=[], strip_comment_tags=False) + options, args = parser.parse_args(argv) + if not args: + parser.error('incorrect number of arguments') + + if options.output not in (None, '-'): + outfile = open(options.output, 'w') + else: + outfile = sys.stdout + + keywords = DEFAULT_KEYWORDS.copy() + if options.no_default_keywords: + if not options.keywords: + parser.error('you must specify new keywords if you disable the ' + 'default ones') + keywords = {} + if options.keywords: + keywords.update(parse_keywords(options.keywords)) + + if options.mapping_file: + fileobj = open(options.mapping_file, 'U') + try: + method_map, options_map = parse_mapping(fileobj) + finally: + fileobj.close() + else: + method_map = DEFAULT_MAPPING + options_map = {} + + if options.width and options.no_wrap: + parser.error("'--no-wrap' and '--width' are mutually exclusive.") + elif not options.width and not options.no_wrap: + options.width = 76 + + if options.sort_output and options.sort_by_file: + parser.error("'--sort-output' and '--sort-by-file' are mutually " + "exclusive") + + try: + catalog = Catalog(project=options.project, + version=options.version, + msgid_bugs_address=options.msgid_bugs_address, + copyright_holder=options.copyright_holder, + charset=options.charset) + + for dirname in args: + if not os.path.isdir(dirname): + parser.error('%r is not a directory' % dirname) + + def callback(filename, method, options): + if method == 'ignore': + return + filepath = os.path.normpath(os.path.join(dirname, filename)) + optstr = '' + if options: + optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for + k, v in options.items()]) + self.log.info('extracting messages from %s%s', filepath, + optstr) + + extracted = extract_from_dir(dirname, method_map, options_map, + keywords, options.comment_tags, + callback=callback, + strip_comment_tags= + options.strip_comment_tags) + for filename, lineno, message, comments in extracted: + filepath = os.path.normpath(os.path.join(dirname, filename)) + catalog.add(message, None, [(filepath, lineno)], + auto_comments=comments) + + if options.output not in (None, '-'): + self.log.info('writing PO template file to %s' % options.output) + write_po(outfile, catalog, width=options.width, + no_location=options.no_location, + omit_header=options.omit_header, + sort_output=options.sort_output, + sort_by_file=options.sort_by_file) + finally: + if options.output: + outfile.close() + + def init(self, argv): + """Subcommand for creating new message catalogs from a template. + + :param argv: the command arguments + """ + parser = OptionParser(usage=self.usage % ('init', ''), + description=self.commands['init']) + parser.add_option('--domain', '-D', dest='domain', + help="domain of PO file (default '%default')") + parser.add_option('--input-file', '-i', dest='input_file', + metavar='FILE', help='name of the input file') + parser.add_option('--output-dir', '-d', dest='output_dir', + metavar='DIR', help='path to output directory') + parser.add_option('--output-file', '-o', dest='output_file', + metavar='FILE', + help="name of the output file (default " + "'//LC_MESSAGES/" + ".po')") + parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', + help='locale for the new localized catalog') + + parser.set_defaults(domain='messages') + options, args = parser.parse_args(argv) + + if not options.locale: + parser.error('you must provide a locale for the new catalog') + try: + locale = Locale.parse(options.locale) + except UnknownLocaleError, e: + parser.error(e) + + if not options.input_file: + parser.error('you must specify the input file') + + if not options.output_file and not options.output_dir: + parser.error('you must specify the output file or directory') + + if not options.output_file: + options.output_file = os.path.join(options.output_dir, + options.locale, 'LC_MESSAGES', + options.domain + '.po') + if not os.path.exists(os.path.dirname(options.output_file)): + os.makedirs(os.path.dirname(options.output_file)) + + infile = open(options.input_file, 'r') + try: + # Although reading from the catalog template, read_po must be fed + # the locale in order to correcly calculate plurals + catalog = read_po(infile, locale=options.locale) + finally: + infile.close() + + catalog.locale = locale + catalog.revision_date = datetime.now(LOCALTZ) + + self.log.info('creating catalog %r based on %r', options.output_file, + options.input_file) + + outfile = open(options.output_file, 'w') + try: + write_po(outfile, catalog) + finally: + outfile.close() + + def update(self, argv): + """Subcommand for updating existing message catalogs from a template. + + :param argv: the command arguments + :since: version 0.9 + """ + parser = OptionParser(usage=self.usage % ('update', ''), + description=self.commands['update']) + parser.add_option('--domain', '-D', dest='domain', + help="domain of PO file (default '%default')") + parser.add_option('--input-file', '-i', dest='input_file', + metavar='FILE', help='name of the input file') + parser.add_option('--output-dir', '-d', dest='output_dir', + metavar='DIR', help='path to output directory') + parser.add_option('--output-file', '-o', dest='output_file', + metavar='FILE', + help="name of the output file (default " + "'//LC_MESSAGES/" + ".po')") + parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', + help='locale of the translations catalog') + parser.add_option('--ignore-obsolete', dest='ignore_obsolete', + action='store_true', + help='do not include obsolete messages in the output ' + '(default %default)') + parser.add_option('--no-fuzzy-matching', '-N', dest='no_fuzzy_matching', + action='store_true', + help='do not use fuzzy matching (default %default)') + parser.add_option('--previous', dest='previous', action='store_true', + help='keep previous msgids of translated messages ' + '(default %default)') + + parser.set_defaults(domain='messages', ignore_obsolete=False, + no_fuzzy_matching=False, previous=False) + options, args = parser.parse_args(argv) + + if not options.input_file: + parser.error('you must specify the input file') + if not options.output_file and not options.output_dir: + parser.error('you must specify the output file or directory') + if options.output_file and not options.locale: + parser.error('you must specify the locale') + if options.no_fuzzy_matching and options.previous: + options.previous = False + + po_files = [] + if not options.output_file: + if options.locale: + po_files.append((options.locale, + os.path.join(options.output_dir, + options.locale, 'LC_MESSAGES', + options.domain + '.po'))) + else: + for locale in os.listdir(options.output_dir): + po_file = os.path.join(options.output_dir, locale, + 'LC_MESSAGES', + options.domain + '.po') + if os.path.exists(po_file): + po_files.append((locale, po_file)) + else: + po_files.append((options.locale, options.output_file)) + + domain = options.domain + if not domain: + domain = os.path.splitext(os.path.basename(options.input_file))[0] + + infile = open(options.input_file, 'U') + try: + template = read_po(infile) + finally: + infile.close() + + if not po_files: + parser.error('no message catalogs found') + + for locale, filename in po_files: + self.log.info('updating catalog %r based on %r', filename, + options.input_file) + infile = open(filename, 'U') + try: + catalog = read_po(infile, locale=locale, domain=domain) + finally: + infile.close() + + catalog.update(template, options.no_fuzzy_matching) + + tmpname = os.path.join(os.path.dirname(filename), + tempfile.gettempprefix() + + os.path.basename(filename)) + tmpfile = open(tmpname, 'w') + try: + try: + write_po(tmpfile, catalog, + ignore_obsolete=options.ignore_obsolete, + include_previous=options.previous) + finally: + tmpfile.close() + except: + os.remove(tmpname) + raise + + try: + os.rename(tmpname, filename) + except OSError: + # We're probably on Windows, which doesn't support atomic + # renames, at least not through Python + # If the error is in fact due to a permissions problem, that + # same error is going to be raised from one of the following + # operations + os.remove(filename) + shutil.copy(tmpname, filename) + os.remove(tmpname) + + +def main(): + return CommandLineInterface().run(sys.argv) + +def parse_mapping(fileobj, filename=None): + """Parse an extraction method mapping from a file-like object. + + >>> buf = StringIO(''' + ... [extractors] + ... custom = mypackage.module:myfunc + ... + ... # Python source files + ... [python: **.py] + ... + ... # Genshi templates + ... [genshi: **/templates/**.html] + ... include_attrs = + ... [genshi: **/templates/**.txt] + ... template_class = genshi.template:TextTemplate + ... encoding = latin-1 + ... + ... # Some custom extractor + ... [custom: **/custom/*.*] + ... ''') + + >>> method_map, options_map = parse_mapping(buf) + >>> len(method_map) + 4 + + >>> method_map[0] + ('**.py', 'python') + >>> options_map['**.py'] + {} + >>> method_map[1] + ('**/templates/**.html', 'genshi') + >>> options_map['**/templates/**.html']['include_attrs'] + '' + >>> method_map[2] + ('**/templates/**.txt', 'genshi') + >>> options_map['**/templates/**.txt']['template_class'] + 'genshi.template:TextTemplate' + >>> options_map['**/templates/**.txt']['encoding'] + 'latin-1' + + >>> method_map[3] + ('**/custom/*.*', 'mypackage.module:myfunc') + >>> options_map['**/custom/*.*'] + {} + + :param fileobj: a readable file-like object containing the configuration + text to parse + :return: a `(method_map, options_map)` tuple + :rtype: `tuple` + :see: `extract_from_directory` + """ + extractors = {} + method_map = [] + options_map = {} + + parser = RawConfigParser() + parser._sections = odict(parser._sections) # We need ordered sections + parser.readfp(fileobj, filename) + for section in parser.sections(): + if section == 'extractors': + extractors = dict(parser.items(section)) + else: + method, pattern = [part.strip() for part in section.split(':', 1)] + method_map.append((pattern, method)) + options_map[pattern] = dict(parser.items(section)) + + if extractors: + for idx, (pattern, method) in enumerate(method_map): + if method in extractors: + method = extractors[method] + method_map[idx] = (pattern, method) + + return (method_map, options_map) + +def parse_keywords(strings=[]): + """Parse keywords specifications from the given list of strings. + + >>> kw = parse_keywords(['_', 'dgettext:2', 'dngettext:2,3']).items() + >>> kw.sort() + >>> for keyword, indices in kw: + ... print (keyword, indices) + ('_', None) + ('dgettext', (2,)) + ('dngettext', (2, 3)) + """ + keywords = {} + for string in strings: + if ':' in string: + funcname, indices = string.split(':') + else: + funcname, indices = string, None + if funcname not in keywords: + if indices: + indices = tuple([(int(x)) for x in indices.split(',')]) + keywords[funcname] = indices + return keywords + + +if __name__ == '__main__': + main() diff --git a/justice/external/babel/messages/mofile.py b/justice/external/babel/messages/mofile.py index bc0f3a8..a1a7d39 100644 --- a/justice/external/babel/messages/mofile.py +++ b/justice/external/babel/messages/mofile.py @@ -1,121 +1,121 @@ -# -*- 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/. - -"""Writing of files in the ``gettext`` MO (machine object) format. - -:since: version 0.9 -:see: `The Format of MO Files - `_ -""" - -import array -import struct - -__all__ = ['write_mo'] -__docformat__ = 'restructuredtext en' - -def write_mo(fileobj, catalog, use_fuzzy=False): - """Write a catalog to the specified file-like object using the GNU MO file - format. - - >>> from babel.messages import Catalog - >>> from gettext import GNUTranslations - >>> from StringIO import StringIO - - >>> catalog = Catalog(locale='en_US') - >>> catalog.add('foo', 'Voh') - >>> catalog.add((u'bar', u'baz'), (u'Bahr', u'Batz')) - >>> catalog.add('fuz', 'Futz', flags=['fuzzy']) - >>> catalog.add('Fizz', '') - >>> catalog.add(('Fuzz', 'Fuzzes'), ('', '')) - >>> buf = StringIO() - - >>> write_mo(buf, catalog) - >>> buf.seek(0) - >>> translations = GNUTranslations(fp=buf) - >>> translations.ugettext('foo') - u'Voh' - >>> translations.ungettext('bar', 'baz', 1) - u'Bahr' - >>> translations.ungettext('bar', 'baz', 2) - u'Batz' - >>> translations.ugettext('fuz') - u'fuz' - >>> translations.ugettext('Fizz') - u'Fizz' - >>> translations.ugettext('Fuzz') - u'Fuzz' - >>> translations.ugettext('Fuzzes') - u'Fuzzes' - - :param fileobj: the file-like object to write to - :param catalog: the `Catalog` instance - :param use_fuzzy: whether translations marked as "fuzzy" should be included - in the output - """ - messages = list(catalog) - if not use_fuzzy: - messages[1:] = [m for m in messages[1:] if not m.fuzzy] - messages.sort() - - ids = strs = '' - offsets = [] - - for message in messages: - # For each string, we need size and file offset. Each string is NUL - # terminated; the NUL does not count into the size. - if message.pluralizable: - msgid = '\x00'.join([ - msgid.encode(catalog.charset) for msgid in message.id - ]) - msgstrs = [] - for idx, string in enumerate(message.string): - if not string: - msgstrs.append(message.id[min(int(idx), 1)]) - else: - msgstrs.append(string) - msgstr = '\x00'.join([ - msgstr.encode(catalog.charset) for msgstr in msgstrs - ]) - else: - msgid = message.id.encode(catalog.charset) - if not message.string: - msgstr = message.id.encode(catalog.charset) - else: - msgstr = message.string.encode(catalog.charset) - offsets.append((len(ids), len(msgid), len(strs), len(msgstr))) - ids += msgid + '\x00' - strs += msgstr + '\x00' - - # The header is 7 32-bit unsigned integers. We don't use hash tables, so - # the keys start right after the index tables. - keystart = 7 * 4 + 16 * len(messages) - valuestart = keystart + len(ids) - - # The string table first has the list of keys, then the list of values. - # Each entry has first the size of the string, then the file offset. - koffsets = [] - voffsets = [] - for o1, l1, o2, l2 in offsets: - koffsets += [l1, o1 + keystart] - voffsets += [l2, o2 + valuestart] - offsets = koffsets + voffsets - - fileobj.write(struct.pack('Iiiiiii', - 0x950412deL, # magic - 0, # version - len(messages), # number of entries - 7 * 4, # start of key index - 7 * 4 + len(messages) * 8, # start of value index - 0, 0 # size and offset of hash table - ) + array.array("i", offsets).tostring() + ids + strs) +# -*- 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/. + +"""Writing of files in the ``gettext`` MO (machine object) format. + +:since: version 0.9 +:see: `The Format of MO Files + `_ +""" + +import array +import struct + +__all__ = ['write_mo'] +__docformat__ = 'restructuredtext en' + +def write_mo(fileobj, catalog, use_fuzzy=False): + """Write a catalog to the specified file-like object using the GNU MO file + format. + + >>> from babel.messages import Catalog + >>> from gettext import GNUTranslations + >>> from StringIO import StringIO + + >>> catalog = Catalog(locale='en_US') + >>> catalog.add('foo', 'Voh') + >>> catalog.add((u'bar', u'baz'), (u'Bahr', u'Batz')) + >>> catalog.add('fuz', 'Futz', flags=['fuzzy']) + >>> catalog.add('Fizz', '') + >>> catalog.add(('Fuzz', 'Fuzzes'), ('', '')) + >>> buf = StringIO() + + >>> write_mo(buf, catalog) + >>> buf.seek(0) + >>> translations = GNUTranslations(fp=buf) + >>> translations.ugettext('foo') + u'Voh' + >>> translations.ungettext('bar', 'baz', 1) + u'Bahr' + >>> translations.ungettext('bar', 'baz', 2) + u'Batz' + >>> translations.ugettext('fuz') + u'fuz' + >>> translations.ugettext('Fizz') + u'Fizz' + >>> translations.ugettext('Fuzz') + u'Fuzz' + >>> translations.ugettext('Fuzzes') + u'Fuzzes' + + :param fileobj: the file-like object to write to + :param catalog: the `Catalog` instance + :param use_fuzzy: whether translations marked as "fuzzy" should be included + in the output + """ + messages = list(catalog) + if not use_fuzzy: + messages[1:] = [m for m in messages[1:] if not m.fuzzy] + messages.sort() + + ids = strs = '' + offsets = [] + + for message in messages: + # For each string, we need size and file offset. Each string is NUL + # terminated; the NUL does not count into the size. + if message.pluralizable: + msgid = '\x00'.join([ + msgid.encode(catalog.charset) for msgid in message.id + ]) + msgstrs = [] + for idx, string in enumerate(message.string): + if not string: + msgstrs.append(message.id[min(int(idx), 1)]) + else: + msgstrs.append(string) + msgstr = '\x00'.join([ + msgstr.encode(catalog.charset) for msgstr in msgstrs + ]) + else: + msgid = message.id.encode(catalog.charset) + if not message.string: + msgstr = message.id.encode(catalog.charset) + else: + msgstr = message.string.encode(catalog.charset) + offsets.append((len(ids), len(msgid), len(strs), len(msgstr))) + ids += msgid + '\x00' + strs += msgstr + '\x00' + + # The header is 7 32-bit unsigned integers. We don't use hash tables, so + # the keys start right after the index tables. + keystart = 7 * 4 + 16 * len(messages) + valuestart = keystart + len(ids) + + # The string table first has the list of keys, then the list of values. + # Each entry has first the size of the string, then the file offset. + koffsets = [] + voffsets = [] + for o1, l1, o2, l2 in offsets: + koffsets += [l1, o1 + keystart] + voffsets += [l2, o2 + valuestart] + offsets = koffsets + voffsets + + fileobj.write(struct.pack('Iiiiiii', + 0x950412deL, # magic + 0, # version + len(messages), # number of entries + 7 * 4, # start of key index + 7 * 4 + len(messages) * 8, # start of value index + 0, 0 # size and offset of hash table + ) + array.array("i", offsets).tostring() + ids + strs) diff --git a/justice/external/babel/messages/plurals.py b/justice/external/babel/messages/plurals.py index 488be41..cfff330 100644 --- a/justice/external/babel/messages/plurals.py +++ b/justice/external/babel/messages/plurals.py @@ -1,255 +1,255 @@ -# -*- 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/. - -"""Plural form definitions.""" - -from babel.core import default_locale, Locale -from babel.util import itemgetter - - -LC_CTYPE = default_locale('LC_CTYPE') - - -PLURALS = { - # Afar - # 'aa': (), - # Abkhazian - # 'ab': (), - # Avestan - # 'ae': (), - # Afrikaans - From Pootle's PO's - 'af': (2, '(n != 1)'), - # Akan - # 'ak': (), - # Amharic - # 'am': (), - # Aragonese - # 'an': (), - # Arabic - From Pootle's PO's - 'ar': (6, '(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n>=3 && n<=10 ? 3 : n>=11 && n<=99 ? 4 : 5)'), - # Assamese - # 'as': (), - # Avaric - # 'av': (), - # Aymara - # 'ay': (), - # Azerbaijani - # 'az': (), - # Bashkir - # 'ba': (), - # Belarusian - # 'be': (), - # Bulgarian - From Pootle's PO's - 'bg': (2, '(n != 1)'), - # Bihari - # 'bh': (), - # Bislama - # 'bi': (), - # Bambara - # 'bm': (), - # Bengali - From Pootle's PO's - 'bn': (2, '(n != 1)'), - # Tibetan - as discussed in private with Andrew West - 'bo': (1, '0'), - # Breton - # 'br': (), - # Bosnian - # 'bs': (), - # Catalan - From Pootle's PO's - 'ca': (2, '(n != 1)'), - # Chechen - # 'ce': (), - # Chamorro - # 'ch': (), - # Corsican - # 'co': (), - # Cree - # 'cr': (), - # Czech - 'cs': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), - # Church Slavic - # 'cu': (), - # Chuvash - 'cv': (1, '0'), - # Welsh - 'cy': (5, '(n==1 ? 1 : n==2 ? 2 : n==3 ? 3 : n==6 ? 4 : 0)'), - # Danish - 'da': (2, '(n != 1)'), - # German - 'de': (2, '(n != 1)'), - # Divehi - # 'dv': (), - # Dzongkha - 'dz': (1, '0'), - # Greek - 'el': (2, '(n != 1)'), - # English - 'en': (2, '(n != 1)'), - # Esperanto - 'eo': (2, '(n != 1)'), - # Spanish - 'es': (2, '(n != 1)'), - # Estonian - 'et': (2, '(n != 1)'), - # Basque - From Pootle's PO's - 'eu': (2, '(n != 1)'), - # Persian - From Pootle's PO's - 'fa': (1, '0'), - # Finnish - 'fi': (2, '(n != 1)'), - # French - 'fr': (2, '(n > 1)'), - # Friulian - From Pootle's PO's - 'fur': (2, '(n > 1)'), - # Irish - 'ga': (3, '(n==1 ? 0 : n==2 ? 1 : 2)'), - # Galician - From Pootle's PO's - 'gl': (2, '(n != 1)'), - # Hausa - From Pootle's PO's - 'ha': (2, '(n != 1)'), - # Hebrew - 'he': (2, '(n != 1)'), - # Hindi - From Pootle's PO's - 'hi': (2, '(n != 1)'), - # Croatian - 'hr': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), - # Hungarian - 'hu': (1, '0'), - # Armenian - From Pootle's PO's - 'hy': (1, '0'), - # Icelandic - From Pootle's PO's - 'is': (2, '(n != 1)'), - # Italian - 'it': (2, '(n != 1)'), - # Japanese - 'ja': (1, '0'), - # Georgian - From Pootle's PO's - 'ka': (1, '0'), - # Kongo - From Pootle's PO's - 'kg': (2, '(n != 1)'), - # Khmer - From Pootle's PO's - 'km': (1, '0'), - # Korean - 'ko': (1, '0'), - # Kurdish - From Pootle's PO's - 'ku': (2, '(n != 1)'), - # Lao - Another member of the Tai language family, like Thai. - 'lo': (1, '0'), - # Lithuanian - 'lt': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2)'), - # Latvian - 'lv': (3, '(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2)'), - # Maltese - From Pootle's PO's - 'mt': (4, '(n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3)'), - # Norwegian Bokmål - 'nb': (2, '(n != 1)'), - # Dutch - 'nl': (2, '(n != 1)'), - # Norwegian Nynorsk - 'nn': (2, '(n != 1)'), - # Norwegian - 'no': (2, '(n != 1)'), - # Punjabi - From Pootle's PO's - 'pa': (2, '(n != 1)'), - # Polish - 'pl': (3, '(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), - # Portuguese - 'pt': (2, '(n != 1)'), - # Brazilian - 'pt_BR': (2, '(n > 1)'), - # Romanian - From Pootle's PO's - 'ro': (3, '(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2)'), - # Russian - 'ru': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), - # Slovak - 'sk': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), - # Slovenian - 'sl': (4, '(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3)'), - # Serbian - From Pootle's PO's - 'sr': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), - # Southern Sotho - From Pootle's PO's - 'st': (2, '(n != 1)'), - # Swedish - 'sv': (2, '(n != 1)'), - # Thai - 'th': (1, '0'), - # Turkish - 'tr': (1, '0'), - # Ukrainian - 'uk': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), - # Venda - From Pootle's PO's - 've': (2, '(n != 1)'), - # Vietnamese - From Pootle's PO's - 'vi': (1, '0'), - # Xhosa - From Pootle's PO's - 'xh': (2, '(n != 1)'), - # Chinese - From Pootle's PO's - 'zh_CN': (1, '0'), - 'zh_HK': (1, '0'), - 'zh_TW': (1, '0'), -} - - -DEFAULT_PLURAL = (2, '(n != 1)') - - -class _PluralTuple(tuple): - """A tuple with plural information.""" - - __slots__ = () - num_plurals = property(itemgetter(0), doc=""" - The number of plurals used by the locale.""") - plural_expr = property(itemgetter(1), doc=""" - The plural expression used by the locale.""") - plural_forms = property(lambda x: 'npurals=%s; plural=%s' % x, doc=""" - The plural expression used by the catalog or locale.""") - - def __str__(self): - return self.plural_forms - - -def get_plural(locale=LC_CTYPE): - """A tuple with the information catalogs need to perform proper - pluralization. The first item of the tuple is the number of plural - forms, the second the plural expression. - - >>> get_plural(locale='en') - (2, '(n != 1)') - >>> get_plural(locale='ga') - (3, '(n==1 ? 0 : n==2 ? 1 : 2)') - - The object returned is a special tuple with additional members: - - >>> tup = get_plural("ja") - >>> tup.num_plurals - 1 - >>> tup.plural_expr - '0' - >>> tup.plural_forms - 'npurals=1; plural=0' - - Converting the tuple into a string prints the plural forms for a - gettext catalog: - - >>> str(tup) - 'npurals=1; plural=0' - """ - locale = Locale.parse(locale) - try: - tup = PLURALS[str(locale)] - except KeyError: - try: - tup = PLURALS[locale.language] - except KeyError: - tup = DEFAULT_PLURAL - return _PluralTuple(tup) +# -*- 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/. + +"""Plural form definitions.""" + +from babel.core import default_locale, Locale +from babel.util import itemgetter + + +LC_CTYPE = default_locale('LC_CTYPE') + + +PLURALS = { + # Afar + # 'aa': (), + # Abkhazian + # 'ab': (), + # Avestan + # 'ae': (), + # Afrikaans - From Pootle's PO's + 'af': (2, '(n != 1)'), + # Akan + # 'ak': (), + # Amharic + # 'am': (), + # Aragonese + # 'an': (), + # Arabic - From Pootle's PO's + 'ar': (6, '(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n>=3 && n<=10 ? 3 : n>=11 && n<=99 ? 4 : 5)'), + # Assamese + # 'as': (), + # Avaric + # 'av': (), + # Aymara + # 'ay': (), + # Azerbaijani + # 'az': (), + # Bashkir + # 'ba': (), + # Belarusian + # 'be': (), + # Bulgarian - From Pootle's PO's + 'bg': (2, '(n != 1)'), + # Bihari + # 'bh': (), + # Bislama + # 'bi': (), + # Bambara + # 'bm': (), + # Bengali - From Pootle's PO's + 'bn': (2, '(n != 1)'), + # Tibetan - as discussed in private with Andrew West + 'bo': (1, '0'), + # Breton + # 'br': (), + # Bosnian + # 'bs': (), + # Catalan - From Pootle's PO's + 'ca': (2, '(n != 1)'), + # Chechen + # 'ce': (), + # Chamorro + # 'ch': (), + # Corsican + # 'co': (), + # Cree + # 'cr': (), + # Czech + 'cs': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Church Slavic + # 'cu': (), + # Chuvash + 'cv': (1, '0'), + # Welsh + 'cy': (5, '(n==1 ? 1 : n==2 ? 2 : n==3 ? 3 : n==6 ? 4 : 0)'), + # Danish + 'da': (2, '(n != 1)'), + # German + 'de': (2, '(n != 1)'), + # Divehi + # 'dv': (), + # Dzongkha + 'dz': (1, '0'), + # Greek + 'el': (2, '(n != 1)'), + # English + 'en': (2, '(n != 1)'), + # Esperanto + 'eo': (2, '(n != 1)'), + # Spanish + 'es': (2, '(n != 1)'), + # Estonian + 'et': (2, '(n != 1)'), + # Basque - From Pootle's PO's + 'eu': (2, '(n != 1)'), + # Persian - From Pootle's PO's + 'fa': (1, '0'), + # Finnish + 'fi': (2, '(n != 1)'), + # French + 'fr': (2, '(n > 1)'), + # Friulian - From Pootle's PO's + 'fur': (2, '(n > 1)'), + # Irish + 'ga': (3, '(n==1 ? 0 : n==2 ? 1 : 2)'), + # Galician - From Pootle's PO's + 'gl': (2, '(n != 1)'), + # Hausa - From Pootle's PO's + 'ha': (2, '(n != 1)'), + # Hebrew + 'he': (2, '(n != 1)'), + # Hindi - From Pootle's PO's + 'hi': (2, '(n != 1)'), + # Croatian + 'hr': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Hungarian + 'hu': (1, '0'), + # Armenian - From Pootle's PO's + 'hy': (1, '0'), + # Icelandic - From Pootle's PO's + 'is': (2, '(n != 1)'), + # Italian + 'it': (2, '(n != 1)'), + # Japanese + 'ja': (1, '0'), + # Georgian - From Pootle's PO's + 'ka': (1, '0'), + # Kongo - From Pootle's PO's + 'kg': (2, '(n != 1)'), + # Khmer - From Pootle's PO's + 'km': (1, '0'), + # Korean + 'ko': (1, '0'), + # Kurdish - From Pootle's PO's + 'ku': (2, '(n != 1)'), + # Lao - Another member of the Tai language family, like Thai. + 'lo': (1, '0'), + # Lithuanian + 'lt': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Latvian + 'lv': (3, '(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2)'), + # Maltese - From Pootle's PO's + 'mt': (4, '(n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3)'), + # Norwegian Bokmål + 'nb': (2, '(n != 1)'), + # Dutch + 'nl': (2, '(n != 1)'), + # Norwegian Nynorsk + 'nn': (2, '(n != 1)'), + # Norwegian + 'no': (2, '(n != 1)'), + # Punjabi - From Pootle's PO's + 'pa': (2, '(n != 1)'), + # Polish + 'pl': (3, '(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Portuguese + 'pt': (2, '(n != 1)'), + # Brazilian + 'pt_BR': (2, '(n > 1)'), + # Romanian - From Pootle's PO's + 'ro': (3, '(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2)'), + # Russian + 'ru': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Slovak + 'sk': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Slovenian + 'sl': (4, '(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3)'), + # Serbian - From Pootle's PO's + 'sr': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Southern Sotho - From Pootle's PO's + 'st': (2, '(n != 1)'), + # Swedish + 'sv': (2, '(n != 1)'), + # Thai + 'th': (1, '0'), + # Turkish + 'tr': (1, '0'), + # Ukrainian + 'uk': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Venda - From Pootle's PO's + 've': (2, '(n != 1)'), + # Vietnamese - From Pootle's PO's + 'vi': (1, '0'), + # Xhosa - From Pootle's PO's + 'xh': (2, '(n != 1)'), + # Chinese - From Pootle's PO's + 'zh_CN': (1, '0'), + 'zh_HK': (1, '0'), + 'zh_TW': (1, '0'), +} + + +DEFAULT_PLURAL = (2, '(n != 1)') + + +class _PluralTuple(tuple): + """A tuple with plural information.""" + + __slots__ = () + num_plurals = property(itemgetter(0), doc=""" + The number of plurals used by the locale.""") + plural_expr = property(itemgetter(1), doc=""" + The plural expression used by the locale.""") + plural_forms = property(lambda x: 'npurals=%s; plural=%s' % x, doc=""" + The plural expression used by the catalog or locale.""") + + def __str__(self): + return self.plural_forms + + +def get_plural(locale=LC_CTYPE): + """A tuple with the information catalogs need to perform proper + pluralization. The first item of the tuple is the number of plural + forms, the second the plural expression. + + >>> get_plural(locale='en') + (2, '(n != 1)') + >>> get_plural(locale='ga') + (3, '(n==1 ? 0 : n==2 ? 1 : 2)') + + The object returned is a special tuple with additional members: + + >>> tup = get_plural("ja") + >>> tup.num_plurals + 1 + >>> tup.plural_expr + '0' + >>> tup.plural_forms + 'npurals=1; plural=0' + + Converting the tuple into a string prints the plural forms for a + gettext catalog: + + >>> str(tup) + 'npurals=1; plural=0' + """ + locale = Locale.parse(locale) + try: + tup = PLURALS[str(locale)] + except KeyError: + try: + tup = PLURALS[locale.language] + except KeyError: + tup = DEFAULT_PLURAL + return _PluralTuple(tup) diff --git a/justice/external/babel/messages/pofile.py b/justice/external/babel/messages/pofile.py index 305b2cf..88101d5 100644 --- a/justice/external/babel/messages/pofile.py +++ b/justice/external/babel/messages/pofile.py @@ -1,456 +1,456 @@ -# -*- 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 - `_ -""" - -from datetime import date, datetime -import os -import re - -from babel import __version__ as VERSION -from babel.messages.catalog import Catalog, Message -from babel.util import set, wraptext, LOCALTZ - -__all__ = ['read_po', 'write_po'] -__docformat__ = 'restructuredtext en' - -def unescape(string): - r"""Reverse `escape` the given string. - - >>> print unescape('"Say:\\n \\"hello, world!\\"\\n"') - Say: - "hello, world!" - - - :param string: the string to unescape - :return: the unescaped string - :rtype: `str` or `unicode` - """ - return string[1:-1].replace('\\\\', '\\') \ - .replace('\\t', '\t') \ - .replace('\\r', '\r') \ - .replace('\\n', '\n') \ - .replace('\\"', '\"') - -def denormalize(string): - r"""Reverse the normalization done by the `normalize` function. - - >>> print denormalize(r'''"" - ... "Say:\n" - ... " \"hello, world!\"\n"''') - Say: - "hello, world!" - - - >>> print denormalize(r'''"" - ... "Say:\n" - ... " \"Lorem ipsum dolor sit " - ... "amet, consectetur adipisicing" - ... " elit, \"\n"''') - Say: - "Lorem ipsum dolor sit amet, consectetur adipisicing elit, " - - - :param string: the string to denormalize - :return: the denormalized string - :rtype: `unicode` or `str` - """ - if string.startswith('""'): - lines = [] - for line in string.splitlines()[1:]: - lines.append(unescape(line)) - return ''.join(lines) - else: - return unescape(string) - -def read_po(fileobj, locale=None, domain=None, ignore_obsolete=False): - """Read messages from a ``gettext`` PO (portable object) file from the given - file-like object and return a `Catalog`. - - >>> from StringIO import StringIO - >>> buf = StringIO(''' - ... #: main.py:1 - ... #, fuzzy, python-format - ... msgid "foo %(name)s" - ... msgstr "" - ... - ... # A user comment - ... #. An auto comment - ... #: main.py:3 - ... msgid "bar" - ... msgid_plural "baz" - ... msgstr[0] "" - ... msgstr[1] "" - ... ''') - >>> catalog = read_po(buf) - >>> catalog.revision_date = datetime(2007, 04, 01) - - >>> for message in catalog: - ... if message.id: - ... print (message.id, message.string) - ... print ' ', (message.locations, message.flags) - ... print ' ', (message.user_comments, message.auto_comments) - (u'foo %(name)s', '') - ([(u'main.py', 1)], set([u'fuzzy', u'python-format'])) - ([], []) - ((u'bar', u'baz'), ('', '')) - ([(u'main.py', 3)], set([])) - ([u'A user comment'], [u'An auto comment']) - - :param fileobj: the file-like object to read the PO file from - :param locale: the locale identifier or `Locale` object, or `None` - if the catalog is not bound to a locale (which basically - means it's a template) - :param domain: the message domain - :param ignore_obsolete: whether to ignore obsolete messages in the input - :return: an iterator over ``(message, translation, location)`` tuples - :rtype: ``iterator`` - """ - catalog = Catalog(locale=locale, domain=domain) - - counter = [0] - offset = [0] - messages = [] - translations = [] - locations = [] - flags = [] - user_comments = [] - auto_comments = [] - obsolete = [False] - in_msgid = [False] - in_msgstr = [False] - - def _add_message(): - translations.sort() - if len(messages) > 1: - msgid = tuple([denormalize(m) for m in messages]) - else: - msgid = denormalize(messages[0]) - if isinstance(msgid, (list, tuple)): - string = [] - for idx in range(catalog.num_plurals): - try: - string.append(translations[idx]) - except IndexError: - string.append((idx, '')) - string = tuple([denormalize(t[1]) for t in string]) - else: - string = denormalize(translations[0][1]) - message = Message(msgid, string, list(locations), set(flags), - auto_comments, user_comments, lineno=offset[0] + 1) - if obsolete[0]: - if not ignore_obsolete: - catalog.obsolete[msgid] = message - else: - catalog[msgid] = message - del messages[:]; del translations[:]; del locations[:]; - del flags[:]; del auto_comments[:]; del user_comments[:] - obsolete[0] = False - counter[0] += 1 - - def _process_message_line(lineno, line): - if line.startswith('msgid_plural'): - in_msgid[0] = True - msg = line[12:].lstrip() - messages.append(msg) - elif line.startswith('msgid'): - in_msgid[0] = True - offset[0] = lineno - txt = line[5:].lstrip() - if messages: - _add_message() - messages.append(txt) - elif line.startswith('msgstr'): - in_msgid[0] = False - in_msgstr[0] = True - msg = line[6:].lstrip() - if msg.startswith('['): - idx, msg = msg[1:].split(']', 1) - translations.append([int(idx), msg.lstrip()]) - else: - translations.append([0, msg]) - elif line.startswith('"'): - if in_msgid[0]: - messages[-1] += u'\n' + line.rstrip() - elif in_msgstr[0]: - translations[-1][1] += u'\n' + line.rstrip() - - for lineno, line in enumerate(fileobj.readlines()): - line = line.strip() - if not isinstance(line, unicode): - line = line.decode(catalog.charset) - if line.startswith('#'): - in_msgid[0] = in_msgstr[0] = False - if messages and translations: - _add_message() - if line[1:].startswith(':'): - for location in line[2:].lstrip().split(): - pos = location.rfind(':') - if pos >= 0: - try: - lineno = int(location[pos + 1:]) - except ValueError: - continue - locations.append((location[:pos], lineno)) - elif line[1:].startswith(','): - for flag in line[2:].lstrip().split(','): - flags.append(flag.strip()) - elif line[1:].startswith('~'): - obsolete[0] = True - _process_message_line(lineno, line[2:].lstrip()) - elif line[1:].startswith('.'): - # These are called auto-comments - comment = line[2:].strip() - if comment: # Just check that we're not adding empty comments - auto_comments.append(comment) - else: - # These are called user comments - user_comments.append(line[1:].strip()) - else: - _process_message_line(lineno, line) - - if messages: - _add_message() - - # No actual messages found, but there was some info in comments, from which - # we'll construct an empty header message - elif not counter[0] and (flags or user_comments or auto_comments): - messages.append(u'') - translations.append([0, u'']) - _add_message() - - return catalog - -WORD_SEP = re.compile('(' - r'\s+|' # any whitespace - r'[^\s\w]*\w+[a-zA-Z]-(?=\w+[a-zA-Z])|' # hyphenated words - r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w)' # em-dash -')') - -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 '"%s"' % string.replace('\\', '\\\\') \ - .replace('\t', '\\t') \ - .replace('\r', '\\r') \ - .replace('\n', '\\n') \ - .replace('\"', '\\"') - -def normalize(string, prefix='', width=76): - r"""Convert a string into a format that is appropriate for .po files. - - >>> print normalize('''Say: - ... "hello, world!" - ... ''', width=None) - "" - "Say:\n" - " \"hello, world!\"\n" - - >>> print normalize('''Say: - ... "Lorem ipsum dolor sit amet, consectetur adipisicing elit, " - ... ''', width=32) - "" - "Say:\n" - " \"Lorem ipsum dolor sit " - "amet, consectetur adipisicing" - " elit, \"\n" - - :param string: the string to normalize - :param prefix: a string that should be prepended to every line - :param width: the maximum line width; use `None`, 0, or a negative number - to completely disable line wrapping - :return: the normalized string - :rtype: `unicode` - """ - if width and width > 0: - prefixlen = len(prefix) - lines = [] - for idx, line in enumerate(string.splitlines(True)): - if len(escape(line)) + prefixlen > width: - chunks = WORD_SEP.split(line) - chunks.reverse() - while chunks: - buf = [] - size = 2 - while chunks: - l = len(escape(chunks[-1])) - 2 + prefixlen - if size + l < width: - buf.append(chunks.pop()) - size += l - else: - if not buf: - # handle long chunks by putting them on a - # separate line - buf.append(chunks.pop()) - break - lines.append(u''.join(buf)) - else: - lines.append(line) - else: - lines = string.splitlines(True) - - if len(lines) <= 1: - return escape(string) - - # Remove empty trailing line - if lines and not lines[-1]: - del lines[-1] - lines[-1] += '\n' - return u'""\n' + u'\n'.join([(prefix + escape(l)) for l in lines]) - -def write_po(fileobj, catalog, width=76, no_location=False, omit_header=False, - sort_output=False, sort_by_file=False, ignore_obsolete=False, - include_previous=False): - r"""Write a ``gettext`` PO (portable object) template file for a given - message catalog to the provided file-like object. - - >>> catalog = Catalog() - >>> catalog.add(u'foo %(name)s', locations=[('main.py', 1)], - ... flags=('fuzzy',)) - >>> catalog.add((u'bar', u'baz'), locations=[('main.py', 3)]) - >>> from StringIO import StringIO - >>> buf = StringIO() - >>> write_po(buf, catalog, omit_header=True) - >>> print buf.getvalue() - #: main.py:1 - #, fuzzy, python-format - msgid "foo %(name)s" - msgstr "" - - #: main.py:3 - msgid "bar" - msgid_plural "baz" - msgstr[0] "" - msgstr[1] "" - - - - :param fileobj: the file-like object to write to - :param catalog: the `Catalog` instance - :param width: the maximum line width for the generated output; use `None`, - 0, or a negative number to completely disable line wrapping - :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 - :param sort_output: whether to sort the messages in the output by msgid - :param sort_by_file: whether to sort the messages in the output by their - locations - :param ignore_obsolete: whether to ignore obsolete messages and not include - them in the output; by default they are included as - comments - :param include_previous: include the old msgid as a comment when - updating the catalog - """ - def _normalize(key, prefix=''): - return normalize(key, prefix=prefix, width=width) \ - .encode(catalog.charset, 'backslashreplace') - - def _write(text): - if isinstance(text, unicode): - text = text.encode(catalog.charset) - fileobj.write(text) - - def _write_comment(comment, prefix=''): - # xgettext always wraps comments even if --no-wrap is passed; - # provide the same behaviour - if width and width > 0: - _width = width - else: - _width = 76 - for line in wraptext(comment, _width): - _write('#%s %s\n' % (prefix, line.strip())) - - def _write_message(message, prefix=''): - if isinstance(message.id, (list, tuple)): - _write('%smsgid %s\n' % (prefix, _normalize(message.id[0], prefix))) - _write('%smsgid_plural %s\n' % ( - prefix, _normalize(message.id[1], prefix) - )) - - for idx in range(catalog.num_plurals): - try: - string = message.string[idx] - except IndexError: - string = '' - _write('%smsgstr[%d] %s\n' % ( - prefix, idx, _normalize(string, prefix) - )) - else: - _write('%smsgid %s\n' % (prefix, _normalize(message.id, prefix))) - _write('%smsgstr %s\n' % ( - prefix, _normalize(message.string or '', prefix) - )) - - messages = list(catalog) - if sort_output: - messages.sort() - elif sort_by_file: - messages.sort(lambda x,y: cmp(x.locations, y.locations)) - - for message in messages: - if not message.id: # This is the header "message" - if omit_header: - continue - comment_header = catalog.header_comment - if width and width > 0: - lines = [] - for line in comment_header.splitlines(): - lines += wraptext(line, width=width, - subsequent_indent='# ') - comment_header = u'\n'.join(lines) + u'\n' - _write(comment_header) - - for comment in message.user_comments: - _write_comment(comment) - for comment in message.auto_comments: - _write_comment(comment, prefix='.') - - if not no_location: - locs = u' '.join([u'%s:%d' % (filename.replace(os.sep, '/'), lineno) - for filename, lineno in message.locations]) - _write_comment(locs, prefix=':') - if message.flags: - _write('#%s\n' % ', '.join([''] + list(message.flags))) - - if message.previous_id and include_previous: - _write_comment('msgid %s' % _normalize(message.previous_id[0]), - prefix='|') - if len(message.previous_id) > 1: - _write_comment('msgid_plural %s' % _normalize( - message.previous_id[1] - ), prefix='|') - - _write_message(message) - _write('\n') - - if not ignore_obsolete: - for message in catalog.obsolete.values(): - for comment in message.user_comments: - _write_comment(comment) - _write_message(message, prefix='#~ ') - _write('\n') +# -*- 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 + `_ +""" + +from datetime import date, datetime +import os +import re + +from babel import __version__ as VERSION +from babel.messages.catalog import Catalog, Message +from babel.util import set, wraptext, LOCALTZ + +__all__ = ['read_po', 'write_po'] +__docformat__ = 'restructuredtext en' + +def unescape(string): + r"""Reverse `escape` the given string. + + >>> print unescape('"Say:\\n \\"hello, world!\\"\\n"') + Say: + "hello, world!" + + + :param string: the string to unescape + :return: the unescaped string + :rtype: `str` or `unicode` + """ + return string[1:-1].replace('\\\\', '\\') \ + .replace('\\t', '\t') \ + .replace('\\r', '\r') \ + .replace('\\n', '\n') \ + .replace('\\"', '\"') + +def denormalize(string): + r"""Reverse the normalization done by the `normalize` function. + + >>> print denormalize(r'''"" + ... "Say:\n" + ... " \"hello, world!\"\n"''') + Say: + "hello, world!" + + + >>> print denormalize(r'''"" + ... "Say:\n" + ... " \"Lorem ipsum dolor sit " + ... "amet, consectetur adipisicing" + ... " elit, \"\n"''') + Say: + "Lorem ipsum dolor sit amet, consectetur adipisicing elit, " + + + :param string: the string to denormalize + :return: the denormalized string + :rtype: `unicode` or `str` + """ + if string.startswith('""'): + lines = [] + for line in string.splitlines()[1:]: + lines.append(unescape(line)) + return ''.join(lines) + else: + return unescape(string) + +def read_po(fileobj, locale=None, domain=None, ignore_obsolete=False): + """Read messages from a ``gettext`` PO (portable object) file from the given + file-like object and return a `Catalog`. + + >>> from StringIO import StringIO + >>> buf = StringIO(''' + ... #: main.py:1 + ... #, fuzzy, python-format + ... msgid "foo %(name)s" + ... msgstr "" + ... + ... # A user comment + ... #. An auto comment + ... #: main.py:3 + ... msgid "bar" + ... msgid_plural "baz" + ... msgstr[0] "" + ... msgstr[1] "" + ... ''') + >>> catalog = read_po(buf) + >>> catalog.revision_date = datetime(2007, 04, 01) + + >>> for message in catalog: + ... if message.id: + ... print (message.id, message.string) + ... print ' ', (message.locations, message.flags) + ... print ' ', (message.user_comments, message.auto_comments) + (u'foo %(name)s', '') + ([(u'main.py', 1)], set([u'fuzzy', u'python-format'])) + ([], []) + ((u'bar', u'baz'), ('', '')) + ([(u'main.py', 3)], set([])) + ([u'A user comment'], [u'An auto comment']) + + :param fileobj: the file-like object to read the PO file from + :param locale: the locale identifier or `Locale` object, or `None` + if the catalog is not bound to a locale (which basically + means it's a template) + :param domain: the message domain + :param ignore_obsolete: whether to ignore obsolete messages in the input + :return: an iterator over ``(message, translation, location)`` tuples + :rtype: ``iterator`` + """ + catalog = Catalog(locale=locale, domain=domain) + + counter = [0] + offset = [0] + messages = [] + translations = [] + locations = [] + flags = [] + user_comments = [] + auto_comments = [] + obsolete = [False] + in_msgid = [False] + in_msgstr = [False] + + def _add_message(): + translations.sort() + if len(messages) > 1: + msgid = tuple([denormalize(m) for m in messages]) + else: + msgid = denormalize(messages[0]) + if isinstance(msgid, (list, tuple)): + string = [] + for idx in range(catalog.num_plurals): + try: + string.append(translations[idx]) + except IndexError: + string.append((idx, '')) + string = tuple([denormalize(t[1]) for t in string]) + else: + string = denormalize(translations[0][1]) + message = Message(msgid, string, list(locations), set(flags), + auto_comments, user_comments, lineno=offset[0] + 1) + if obsolete[0]: + if not ignore_obsolete: + catalog.obsolete[msgid] = message + else: + catalog[msgid] = message + del messages[:]; del translations[:]; del locations[:]; + del flags[:]; del auto_comments[:]; del user_comments[:] + obsolete[0] = False + counter[0] += 1 + + def _process_message_line(lineno, line): + if line.startswith('msgid_plural'): + in_msgid[0] = True + msg = line[12:].lstrip() + messages.append(msg) + elif line.startswith('msgid'): + in_msgid[0] = True + offset[0] = lineno + txt = line[5:].lstrip() + if messages: + _add_message() + messages.append(txt) + elif line.startswith('msgstr'): + in_msgid[0] = False + in_msgstr[0] = True + msg = line[6:].lstrip() + if msg.startswith('['): + idx, msg = msg[1:].split(']', 1) + translations.append([int(idx), msg.lstrip()]) + else: + translations.append([0, msg]) + elif line.startswith('"'): + if in_msgid[0]: + messages[-1] += u'\n' + line.rstrip() + elif in_msgstr[0]: + translations[-1][1] += u'\n' + line.rstrip() + + for lineno, line in enumerate(fileobj.readlines()): + line = line.strip() + if not isinstance(line, unicode): + line = line.decode(catalog.charset) + if line.startswith('#'): + in_msgid[0] = in_msgstr[0] = False + if messages and translations: + _add_message() + if line[1:].startswith(':'): + for location in line[2:].lstrip().split(): + pos = location.rfind(':') + if pos >= 0: + try: + lineno = int(location[pos + 1:]) + except ValueError: + continue + locations.append((location[:pos], lineno)) + elif line[1:].startswith(','): + for flag in line[2:].lstrip().split(','): + flags.append(flag.strip()) + elif line[1:].startswith('~'): + obsolete[0] = True + _process_message_line(lineno, line[2:].lstrip()) + elif line[1:].startswith('.'): + # These are called auto-comments + comment = line[2:].strip() + if comment: # Just check that we're not adding empty comments + auto_comments.append(comment) + else: + # These are called user comments + user_comments.append(line[1:].strip()) + else: + _process_message_line(lineno, line) + + if messages: + _add_message() + + # No actual messages found, but there was some info in comments, from which + # we'll construct an empty header message + elif not counter[0] and (flags or user_comments or auto_comments): + messages.append(u'') + translations.append([0, u'']) + _add_message() + + return catalog + +WORD_SEP = re.compile('(' + r'\s+|' # any whitespace + r'[^\s\w]*\w+[a-zA-Z]-(?=\w+[a-zA-Z])|' # hyphenated words + r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w)' # em-dash +')') + +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 '"%s"' % string.replace('\\', '\\\\') \ + .replace('\t', '\\t') \ + .replace('\r', '\\r') \ + .replace('\n', '\\n') \ + .replace('\"', '\\"') + +def normalize(string, prefix='', width=76): + r"""Convert a string into a format that is appropriate for .po files. + + >>> print normalize('''Say: + ... "hello, world!" + ... ''', width=None) + "" + "Say:\n" + " \"hello, world!\"\n" + + >>> print normalize('''Say: + ... "Lorem ipsum dolor sit amet, consectetur adipisicing elit, " + ... ''', width=32) + "" + "Say:\n" + " \"Lorem ipsum dolor sit " + "amet, consectetur adipisicing" + " elit, \"\n" + + :param string: the string to normalize + :param prefix: a string that should be prepended to every line + :param width: the maximum line width; use `None`, 0, or a negative number + to completely disable line wrapping + :return: the normalized string + :rtype: `unicode` + """ + if width and width > 0: + prefixlen = len(prefix) + lines = [] + for idx, line in enumerate(string.splitlines(True)): + if len(escape(line)) + prefixlen > width: + chunks = WORD_SEP.split(line) + chunks.reverse() + while chunks: + buf = [] + size = 2 + while chunks: + l = len(escape(chunks[-1])) - 2 + prefixlen + if size + l < width: + buf.append(chunks.pop()) + size += l + else: + if not buf: + # handle long chunks by putting them on a + # separate line + buf.append(chunks.pop()) + break + lines.append(u''.join(buf)) + else: + lines.append(line) + else: + lines = string.splitlines(True) + + if len(lines) <= 1: + return escape(string) + + # Remove empty trailing line + if lines and not lines[-1]: + del lines[-1] + lines[-1] += '\n' + return u'""\n' + u'\n'.join([(prefix + escape(l)) for l in lines]) + +def write_po(fileobj, catalog, width=76, no_location=False, omit_header=False, + sort_output=False, sort_by_file=False, ignore_obsolete=False, + include_previous=False): + r"""Write a ``gettext`` PO (portable object) template file for a given + message catalog to the provided file-like object. + + >>> catalog = Catalog() + >>> catalog.add(u'foo %(name)s', locations=[('main.py', 1)], + ... flags=('fuzzy',)) + >>> catalog.add((u'bar', u'baz'), locations=[('main.py', 3)]) + >>> from StringIO import StringIO + >>> buf = StringIO() + >>> write_po(buf, catalog, omit_header=True) + >>> print buf.getvalue() + #: main.py:1 + #, fuzzy, python-format + msgid "foo %(name)s" + msgstr "" + + #: main.py:3 + msgid "bar" + msgid_plural "baz" + msgstr[0] "" + msgstr[1] "" + + + + :param fileobj: the file-like object to write to + :param catalog: the `Catalog` instance + :param width: the maximum line width for the generated output; use `None`, + 0, or a negative number to completely disable line wrapping + :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 + :param sort_output: whether to sort the messages in the output by msgid + :param sort_by_file: whether to sort the messages in the output by their + locations + :param ignore_obsolete: whether to ignore obsolete messages and not include + them in the output; by default they are included as + comments + :param include_previous: include the old msgid as a comment when + updating the catalog + """ + def _normalize(key, prefix=''): + return normalize(key, prefix=prefix, width=width) \ + .encode(catalog.charset, 'backslashreplace') + + def _write(text): + if isinstance(text, unicode): + text = text.encode(catalog.charset) + fileobj.write(text) + + def _write_comment(comment, prefix=''): + # xgettext always wraps comments even if --no-wrap is passed; + # provide the same behaviour + if width and width > 0: + _width = width + else: + _width = 76 + for line in wraptext(comment, _width): + _write('#%s %s\n' % (prefix, line.strip())) + + def _write_message(message, prefix=''): + if isinstance(message.id, (list, tuple)): + _write('%smsgid %s\n' % (prefix, _normalize(message.id[0], prefix))) + _write('%smsgid_plural %s\n' % ( + prefix, _normalize(message.id[1], prefix) + )) + + for idx in range(catalog.num_plurals): + try: + string = message.string[idx] + except IndexError: + string = '' + _write('%smsgstr[%d] %s\n' % ( + prefix, idx, _normalize(string, prefix) + )) + else: + _write('%smsgid %s\n' % (prefix, _normalize(message.id, prefix))) + _write('%smsgstr %s\n' % ( + prefix, _normalize(message.string or '', prefix) + )) + + messages = list(catalog) + if sort_output: + messages.sort() + elif sort_by_file: + messages.sort(lambda x,y: cmp(x.locations, y.locations)) + + for message in messages: + if not message.id: # This is the header "message" + if omit_header: + continue + comment_header = catalog.header_comment + if width and width > 0: + lines = [] + for line in comment_header.splitlines(): + lines += wraptext(line, width=width, + subsequent_indent='# ') + comment_header = u'\n'.join(lines) + u'\n' + _write(comment_header) + + for comment in message.user_comments: + _write_comment(comment) + for comment in message.auto_comments: + _write_comment(comment, prefix='.') + + if not no_location: + locs = u' '.join([u'%s:%d' % (filename.replace(os.sep, '/'), lineno) + for filename, lineno in message.locations]) + _write_comment(locs, prefix=':') + if message.flags: + _write('#%s\n' % ', '.join([''] + list(message.flags))) + + if message.previous_id and include_previous: + _write_comment('msgid %s' % _normalize(message.previous_id[0]), + prefix='|') + if len(message.previous_id) > 1: + _write_comment('msgid_plural %s' % _normalize( + message.previous_id[1] + ), prefix='|') + + _write_message(message) + _write('\n') + + if not ignore_obsolete: + for message in catalog.obsolete.values(): + for comment in message.user_comments: + _write_comment(comment) + _write_message(message, prefix='#~ ') + _write('\n') diff --git a/justice/external/babel/messages/tests/__init__.py b/justice/external/babel/messages/tests/__init__.py index a9e0a94..7b1b155 100644 --- a/justice/external/babel/messages/tests/__init__.py +++ b/justice/external/babel/messages/tests/__init__.py @@ -1,29 +1,29 @@ -# -*- 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 unittest - -def suite(): - from babel.messages.tests import catalog, extract, frontend, mofile, \ - plurals, pofile, checkers - suite = unittest.TestSuite() - suite.addTest(catalog.suite()) - suite.addTest(extract.suite()) - suite.addTest(frontend.suite()) - suite.addTest(mofile.suite()) - suite.addTest(pofile.suite()) - suite.addTest(checkers.suite()) - return suite - -if __name__ == '__main__': - unittest.main(defaultTest='suite') +# -*- 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 unittest + +def suite(): + from babel.messages.tests import catalog, extract, frontend, mofile, \ + plurals, pofile, checkers + suite = unittest.TestSuite() + suite.addTest(catalog.suite()) + suite.addTest(extract.suite()) + suite.addTest(frontend.suite()) + suite.addTest(mofile.suite()) + suite.addTest(pofile.suite()) + suite.addTest(checkers.suite()) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/justice/external/babel/messages/tests/catalog.py b/justice/external/babel/messages/tests/catalog.py index 7506e1a..b4eaad2 100644 --- a/justice/external/babel/messages/tests/catalog.py +++ b/justice/external/babel/messages/tests/catalog.py @@ -1,264 +1,264 @@ -# -*- 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 -import datetime -import doctest -import unittest - -from babel.messages import catalog - - -class MessageTestCase(unittest.TestCase): - - def test_python_format(self): - assert catalog.PYTHON_FORMAT.search('foo %d bar') - assert catalog.PYTHON_FORMAT.search('foo %s bar') - assert catalog.PYTHON_FORMAT.search('foo %r bar') - assert catalog.PYTHON_FORMAT.search('foo %(name).1f') - assert catalog.PYTHON_FORMAT.search('foo %(name)3.3f') - assert catalog.PYTHON_FORMAT.search('foo %(name)3f') - assert catalog.PYTHON_FORMAT.search('foo %(name)06d') - assert catalog.PYTHON_FORMAT.search('foo %(name)Li') - assert catalog.PYTHON_FORMAT.search('foo %(name)#d') - assert catalog.PYTHON_FORMAT.search('foo %(name)-4.4hs') - assert catalog.PYTHON_FORMAT.search('foo %(name)*.3f') - assert catalog.PYTHON_FORMAT.search('foo %(name).*f') - assert catalog.PYTHON_FORMAT.search('foo %(name)3.*f') - assert catalog.PYTHON_FORMAT.search('foo %(name)*.*f') - assert catalog.PYTHON_FORMAT.search('foo %()s') - - def test_translator_comments(self): - mess = catalog.Message('foo', user_comments=['Comment About `foo`']) - self.assertEqual(mess.user_comments, ['Comment About `foo`']) - mess = catalog.Message('foo', - auto_comments=['Comment 1 About `foo`', - 'Comment 2 About `foo`']) - self.assertEqual(mess.auto_comments, ['Comment 1 About `foo`', - 'Comment 2 About `foo`']) - - def test_clone_message_object(self): - msg = catalog.Message('foo', locations=[('foo.py', 42)]) - clone = msg.clone() - clone.locations.append(('bar.py', 42)) - self.assertEqual(msg.locations, [('foo.py', 42)]) - msg.flags.add('fuzzy') - assert not clone.fuzzy and msg.fuzzy - - -class CatalogTestCase(unittest.TestCase): - - def test_two_messages_with_same_singular(self): - cat = catalog.Catalog() - cat.add('foo') - cat.add(('foo', 'foos')) - self.assertEqual(1, len(cat)) - - def test_duplicate_auto_comment(self): - cat = catalog.Catalog() - cat.add('foo', auto_comments=['A comment']) - cat.add('foo', auto_comments=['A comment', 'Another comment']) - self.assertEqual(['A comment', 'Another comment'], - cat['foo'].auto_comments) - - def test_duplicate_user_comment(self): - cat = catalog.Catalog() - cat.add('foo', user_comments=['A comment']) - cat.add('foo', user_comments=['A comment', 'Another comment']) - self.assertEqual(['A comment', 'Another comment'], - cat['foo'].user_comments) - - def test_duplicate_location(self): - cat = catalog.Catalog() - cat.add('foo', locations=[('foo.py', 1)]) - cat.add('foo', locations=[('foo.py', 1)]) - self.assertEqual([('foo.py', 1)], cat['foo'].locations) - - def test_update_message_changed_to_plural(self): - cat = catalog.Catalog() - cat.add(u'foo', u'Voh') - tmpl = catalog.Catalog() - tmpl.add((u'foo', u'foos')) - cat.update(tmpl) - self.assertEqual((u'Voh', ''), cat['foo'].string) - assert cat['foo'].fuzzy - - def test_update_message_changed_to_simple(self): - cat = catalog.Catalog() - cat.add((u'foo' u'foos'), (u'Voh', u'Vöhs')) - tmpl = catalog.Catalog() - tmpl.add(u'foo') - cat.update(tmpl) - self.assertEqual(u'Voh', cat['foo'].string) - assert cat['foo'].fuzzy - - def test_update_message_updates_comments(self): - cat = catalog.Catalog() - cat[u'foo'] = catalog.Message('foo', locations=[('main.py', 5)]) - self.assertEqual(cat[u'foo'].auto_comments, []) - self.assertEqual(cat[u'foo'].user_comments, []) - # Update cat[u'foo'] with a new location and a comment - cat[u'foo'] = catalog.Message('foo', locations=[('main.py', 7)], - user_comments=['Foo Bar comment 1']) - self.assertEqual(cat[u'foo'].user_comments, ['Foo Bar comment 1']) - # now add yet another location with another comment - cat[u'foo'] = catalog.Message('foo', locations=[('main.py', 9)], - auto_comments=['Foo Bar comment 2']) - self.assertEqual(cat[u'foo'].auto_comments, ['Foo Bar comment 2']) - - def test_update_fuzzy_matching_with_case_change(self): - cat = catalog.Catalog() - cat.add('foo', 'Voh') - cat.add('bar', 'Bahr') - tmpl = catalog.Catalog() - tmpl.add('Foo') - cat.update(tmpl) - self.assertEqual(1, len(cat.obsolete)) - assert 'foo' not in cat - - self.assertEqual('Voh', cat['Foo'].string) - self.assertEqual(True, cat['Foo'].fuzzy) - - def test_update_fuzzy_matching_with_char_change(self): - cat = catalog.Catalog() - cat.add('fo', 'Voh') - cat.add('bar', 'Bahr') - tmpl = catalog.Catalog() - tmpl.add('foo') - cat.update(tmpl) - self.assertEqual(1, len(cat.obsolete)) - assert 'fo' not in cat - - self.assertEqual('Voh', cat['foo'].string) - self.assertEqual(True, cat['foo'].fuzzy) - - def test_update_fuzzy_matching_no_msgstr(self): - cat = catalog.Catalog() - cat.add('fo', '') - tmpl = catalog.Catalog() - tmpl.add('fo') - tmpl.add('foo') - cat.update(tmpl) - assert 'fo' in cat - assert 'foo' in cat - - self.assertEqual('', cat['fo'].string) - self.assertEqual(False, cat['fo'].fuzzy) - self.assertEqual(None, cat['foo'].string) - self.assertEqual(False, cat['foo'].fuzzy) - - def test_update_fuzzy_matching_no_cascading(self): - cat = catalog.Catalog() - cat.add('fo', 'Voh') - cat.add('foo', 'Vohe') - tmpl = catalog.Catalog() - tmpl.add('fo') - tmpl.add('foo') - tmpl.add('fooo') - cat.update(tmpl) - assert 'fo' in cat - assert 'foo' in cat - - self.assertEqual('Voh', cat['fo'].string) - self.assertEqual(False, cat['fo'].fuzzy) - self.assertEqual('Vohe', cat['foo'].string) - self.assertEqual(False, cat['foo'].fuzzy) - self.assertEqual('Vohe', cat['fooo'].string) - self.assertEqual(True, cat['fooo'].fuzzy) - - def test_update_without_fuzzy_matching(self): - cat = catalog.Catalog() - cat.add('fo', 'Voh') - cat.add('bar', 'Bahr') - tmpl = catalog.Catalog() - tmpl.add('foo') - cat.update(tmpl, no_fuzzy_matching=True) - self.assertEqual(2, len(cat.obsolete)) - - def test_fuzzy_matching_regarding_plurals(self): - cat = catalog.Catalog() - cat.add(('foo', 'foh'), ('foo', 'foh')) - ru = copy.copy(cat) - ru.locale = 'ru_RU' - ru.update(cat) - self.assertEqual(True, ru['foo'].fuzzy) - ru = copy.copy(cat) - ru.locale = 'ru_RU' - ru['foo'].string = ('foh', 'fohh', 'fohhh') - ru.update(cat) - self.assertEqual(False, ru['foo'].fuzzy) - - def test_update_no_template_mutation(self): - tmpl = catalog.Catalog() - tmpl.add('foo') - cat1 = catalog.Catalog() - cat1.add('foo', 'Voh') - cat1.update(tmpl) - cat2 = catalog.Catalog() - cat2.update(tmpl) - - self.assertEqual(None, cat2['foo'].string) - self.assertEqual(False, cat2['foo'].fuzzy) - - def test_update_po_updates_pot_creation_date(self): - template = catalog.Catalog() - localized_catalog = copy.deepcopy(template) - localized_catalog.locale = 'de_DE' - self.assertNotEqual(template.mime_headers, - localized_catalog.mime_headers) - self.assertEqual(template.creation_date, - localized_catalog.creation_date) - template.creation_date = datetime.datetime.now() - \ - datetime.timedelta(minutes=5) - localized_catalog.update(template) - self.assertEqual(template.creation_date, - localized_catalog.creation_date) - - def test_update_po_keeps_po_revision_date(self): - template = catalog.Catalog() - localized_catalog = copy.deepcopy(template) - localized_catalog.locale = 'de_DE' - fake_rev_date = datetime.datetime.now() - datetime.timedelta(days=5) - localized_catalog.revision_date = fake_rev_date - self.assertNotEqual(template.mime_headers, - localized_catalog.mime_headers) - self.assertEqual(template.creation_date, - localized_catalog.creation_date) - template.creation_date = datetime.datetime.now() - \ - datetime.timedelta(minutes=5) - localized_catalog.update(template) - self.assertEqual(localized_catalog.revision_date, fake_rev_date) - - def test_stores_datetime_correctly(self): - localized = catalog.Catalog() - localized.locale = 'de_DE' - localized[''] = catalog.Message('', - "POT-Creation-Date: 2009-03-09 15:47-0700\n" + - "PO-Revision-Date: 2009-03-09 15:47-0700\n") - for key, value in localized.mime_headers: - if key in ('POT-Creation-Date', 'PO-Revision-Date'): - self.assertEqual(value, '2009-03-09 15:47-0700') - -def suite(): - suite = unittest.TestSuite() - if hasattr(doctest, 'ELLIPSIS'): - suite.addTest(doctest.DocTestSuite(catalog, optionflags=doctest.ELLIPSIS)) - else: - # Python 2.3 has no doctest.ELLIPSIS option, it's implicit - suite.addTest(doctest.DocTestSuite(catalog)) - suite.addTest(unittest.makeSuite(MessageTestCase)) - suite.addTest(unittest.makeSuite(CatalogTestCase)) - return suite - -if __name__ == '__main__': - unittest.main(defaultTest='suite') +# -*- 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 +import datetime +import doctest +import unittest + +from babel.messages import catalog + + +class MessageTestCase(unittest.TestCase): + + def test_python_format(self): + assert catalog.PYTHON_FORMAT.search('foo %d bar') + assert catalog.PYTHON_FORMAT.search('foo %s bar') + assert catalog.PYTHON_FORMAT.search('foo %r bar') + assert catalog.PYTHON_FORMAT.search('foo %(name).1f') + assert catalog.PYTHON_FORMAT.search('foo %(name)3.3f') + assert catalog.PYTHON_FORMAT.search('foo %(name)3f') + assert catalog.PYTHON_FORMAT.search('foo %(name)06d') + assert catalog.PYTHON_FORMAT.search('foo %(name)Li') + assert catalog.PYTHON_FORMAT.search('foo %(name)#d') + assert catalog.PYTHON_FORMAT.search('foo %(name)-4.4hs') + assert catalog.PYTHON_FORMAT.search('foo %(name)*.3f') + assert catalog.PYTHON_FORMAT.search('foo %(name).*f') + assert catalog.PYTHON_FORMAT.search('foo %(name)3.*f') + assert catalog.PYTHON_FORMAT.search('foo %(name)*.*f') + assert catalog.PYTHON_FORMAT.search('foo %()s') + + def test_translator_comments(self): + mess = catalog.Message('foo', user_comments=['Comment About `foo`']) + self.assertEqual(mess.user_comments, ['Comment About `foo`']) + mess = catalog.Message('foo', + auto_comments=['Comment 1 About `foo`', + 'Comment 2 About `foo`']) + self.assertEqual(mess.auto_comments, ['Comment 1 About `foo`', + 'Comment 2 About `foo`']) + + def test_clone_message_object(self): + msg = catalog.Message('foo', locations=[('foo.py', 42)]) + clone = msg.clone() + clone.locations.append(('bar.py', 42)) + self.assertEqual(msg.locations, [('foo.py', 42)]) + msg.flags.add('fuzzy') + assert not clone.fuzzy and msg.fuzzy + + +class CatalogTestCase(unittest.TestCase): + + def test_two_messages_with_same_singular(self): + cat = catalog.Catalog() + cat.add('foo') + cat.add(('foo', 'foos')) + self.assertEqual(1, len(cat)) + + def test_duplicate_auto_comment(self): + cat = catalog.Catalog() + cat.add('foo', auto_comments=['A comment']) + cat.add('foo', auto_comments=['A comment', 'Another comment']) + self.assertEqual(['A comment', 'Another comment'], + cat['foo'].auto_comments) + + def test_duplicate_user_comment(self): + cat = catalog.Catalog() + cat.add('foo', user_comments=['A comment']) + cat.add('foo', user_comments=['A comment', 'Another comment']) + self.assertEqual(['A comment', 'Another comment'], + cat['foo'].user_comments) + + def test_duplicate_location(self): + cat = catalog.Catalog() + cat.add('foo', locations=[('foo.py', 1)]) + cat.add('foo', locations=[('foo.py', 1)]) + self.assertEqual([('foo.py', 1)], cat['foo'].locations) + + def test_update_message_changed_to_plural(self): + cat = catalog.Catalog() + cat.add(u'foo', u'Voh') + tmpl = catalog.Catalog() + tmpl.add((u'foo', u'foos')) + cat.update(tmpl) + self.assertEqual((u'Voh', ''), cat['foo'].string) + assert cat['foo'].fuzzy + + def test_update_message_changed_to_simple(self): + cat = catalog.Catalog() + cat.add((u'foo' u'foos'), (u'Voh', u'Vöhs')) + tmpl = catalog.Catalog() + tmpl.add(u'foo') + cat.update(tmpl) + self.assertEqual(u'Voh', cat['foo'].string) + assert cat['foo'].fuzzy + + def test_update_message_updates_comments(self): + cat = catalog.Catalog() + cat[u'foo'] = catalog.Message('foo', locations=[('main.py', 5)]) + self.assertEqual(cat[u'foo'].auto_comments, []) + self.assertEqual(cat[u'foo'].user_comments, []) + # Update cat[u'foo'] with a new location and a comment + cat[u'foo'] = catalog.Message('foo', locations=[('main.py', 7)], + user_comments=['Foo Bar comment 1']) + self.assertEqual(cat[u'foo'].user_comments, ['Foo Bar comment 1']) + # now add yet another location with another comment + cat[u'foo'] = catalog.Message('foo', locations=[('main.py', 9)], + auto_comments=['Foo Bar comment 2']) + self.assertEqual(cat[u'foo'].auto_comments, ['Foo Bar comment 2']) + + def test_update_fuzzy_matching_with_case_change(self): + cat = catalog.Catalog() + cat.add('foo', 'Voh') + cat.add('bar', 'Bahr') + tmpl = catalog.Catalog() + tmpl.add('Foo') + cat.update(tmpl) + self.assertEqual(1, len(cat.obsolete)) + assert 'foo' not in cat + + self.assertEqual('Voh', cat['Foo'].string) + self.assertEqual(True, cat['Foo'].fuzzy) + + def test_update_fuzzy_matching_with_char_change(self): + cat = catalog.Catalog() + cat.add('fo', 'Voh') + cat.add('bar', 'Bahr') + tmpl = catalog.Catalog() + tmpl.add('foo') + cat.update(tmpl) + self.assertEqual(1, len(cat.obsolete)) + assert 'fo' not in cat + + self.assertEqual('Voh', cat['foo'].string) + self.assertEqual(True, cat['foo'].fuzzy) + + def test_update_fuzzy_matching_no_msgstr(self): + cat = catalog.Catalog() + cat.add('fo', '') + tmpl = catalog.Catalog() + tmpl.add('fo') + tmpl.add('foo') + cat.update(tmpl) + assert 'fo' in cat + assert 'foo' in cat + + self.assertEqual('', cat['fo'].string) + self.assertEqual(False, cat['fo'].fuzzy) + self.assertEqual(None, cat['foo'].string) + self.assertEqual(False, cat['foo'].fuzzy) + + def test_update_fuzzy_matching_no_cascading(self): + cat = catalog.Catalog() + cat.add('fo', 'Voh') + cat.add('foo', 'Vohe') + tmpl = catalog.Catalog() + tmpl.add('fo') + tmpl.add('foo') + tmpl.add('fooo') + cat.update(tmpl) + assert 'fo' in cat + assert 'foo' in cat + + self.assertEqual('Voh', cat['fo'].string) + self.assertEqual(False, cat['fo'].fuzzy) + self.assertEqual('Vohe', cat['foo'].string) + self.assertEqual(False, cat['foo'].fuzzy) + self.assertEqual('Vohe', cat['fooo'].string) + self.assertEqual(True, cat['fooo'].fuzzy) + + def test_update_without_fuzzy_matching(self): + cat = catalog.Catalog() + cat.add('fo', 'Voh') + cat.add('bar', 'Bahr') + tmpl = catalog.Catalog() + tmpl.add('foo') + cat.update(tmpl, no_fuzzy_matching=True) + self.assertEqual(2, len(cat.obsolete)) + + def test_fuzzy_matching_regarding_plurals(self): + cat = catalog.Catalog() + cat.add(('foo', 'foh'), ('foo', 'foh')) + ru = copy.copy(cat) + ru.locale = 'ru_RU' + ru.update(cat) + self.assertEqual(True, ru['foo'].fuzzy) + ru = copy.copy(cat) + ru.locale = 'ru_RU' + ru['foo'].string = ('foh', 'fohh', 'fohhh') + ru.update(cat) + self.assertEqual(False, ru['foo'].fuzzy) + + def test_update_no_template_mutation(self): + tmpl = catalog.Catalog() + tmpl.add('foo') + cat1 = catalog.Catalog() + cat1.add('foo', 'Voh') + cat1.update(tmpl) + cat2 = catalog.Catalog() + cat2.update(tmpl) + + self.assertEqual(None, cat2['foo'].string) + self.assertEqual(False, cat2['foo'].fuzzy) + + def test_update_po_updates_pot_creation_date(self): + template = catalog.Catalog() + localized_catalog = copy.deepcopy(template) + localized_catalog.locale = 'de_DE' + self.assertNotEqual(template.mime_headers, + localized_catalog.mime_headers) + self.assertEqual(template.creation_date, + localized_catalog.creation_date) + template.creation_date = datetime.datetime.now() - \ + datetime.timedelta(minutes=5) + localized_catalog.update(template) + self.assertEqual(template.creation_date, + localized_catalog.creation_date) + + def test_update_po_keeps_po_revision_date(self): + template = catalog.Catalog() + localized_catalog = copy.deepcopy(template) + localized_catalog.locale = 'de_DE' + fake_rev_date = datetime.datetime.now() - datetime.timedelta(days=5) + localized_catalog.revision_date = fake_rev_date + self.assertNotEqual(template.mime_headers, + localized_catalog.mime_headers) + self.assertEqual(template.creation_date, + localized_catalog.creation_date) + template.creation_date = datetime.datetime.now() - \ + datetime.timedelta(minutes=5) + localized_catalog.update(template) + self.assertEqual(localized_catalog.revision_date, fake_rev_date) + + def test_stores_datetime_correctly(self): + localized = catalog.Catalog() + localized.locale = 'de_DE' + localized[''] = catalog.Message('', + "POT-Creation-Date: 2009-03-09 15:47-0700\n" + + "PO-Revision-Date: 2009-03-09 15:47-0700\n") + for key, value in localized.mime_headers: + if key in ('POT-Creation-Date', 'PO-Revision-Date'): + self.assertEqual(value, '2009-03-09 15:47-0700') + +def suite(): + suite = unittest.TestSuite() + if hasattr(doctest, 'ELLIPSIS'): + suite.addTest(doctest.DocTestSuite(catalog, optionflags=doctest.ELLIPSIS)) + else: + # Python 2.3 has no doctest.ELLIPSIS option, it's implicit + suite.addTest(doctest.DocTestSuite(catalog)) + suite.addTest(unittest.makeSuite(MessageTestCase)) + suite.addTest(unittest.makeSuite(CatalogTestCase)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/justice/external/babel/messages/tests/checkers.py b/justice/external/babel/messages/tests/checkers.py index b009a5e..3c817d4 100644 --- a/justice/external/babel/messages/tests/checkers.py +++ b/justice/external/babel/messages/tests/checkers.py @@ -1,371 +1,371 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2008 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/. - -from datetime import datetime -import time -import unittest -from StringIO import StringIO - -from babel import __version__ as VERSION -from babel.core import Locale, UnknownLocaleError -from babel.dates import format_datetime -from babel.messages import checkers -from babel.messages.plurals import PLURALS -from babel.messages.pofile import read_po -from babel.util import LOCALTZ - - -class CheckersTestCase(unittest.TestCase): - # the last msgstr[idx] is always missing except for singular plural forms - - def test_1_num_plurals_checkers(self): - for _locale in [p for p in PLURALS if PLURALS[p][0] == 1]: - try: - locale = Locale.parse(_locale) - except UnknownLocaleError: - # Just an alias? Not what we're testing here, let's continue - continue - po_file = (ur"""\ -# %(english_name)s translations for TestProject. -# Copyright (C) 2007 FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , 2007. -# -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: 2007-04-01 15:30+0200\n" -"PO-Revision-Date: %(date)s\n" -"Last-Translator: FULL NAME \n" -"Language-Team: %(locale)s \n" -"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. This will be a translator comment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" - -""" % dict(locale = _locale, - english_name = locale.english_name, - version = VERSION, - year = time.strftime('%Y'), - date = format_datetime(datetime.now(LOCALTZ), - 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale=_locale), - num_plurals = PLURALS[_locale][0], - plural_expr = PLURALS[_locale][0])).encode('utf-8') - - # This test will fail for revisions <= 406 because so far - # catalog.num_plurals was neglected - catalog = read_po(StringIO(po_file), _locale) - message = catalog['foobar'] - checkers.num_plurals(catalog, message) - - def test_2_num_plurals_checkers(self): - # in this testcase we add an extra msgstr[idx], we should be - # disregarding it - for _locale in [p for p in PLURALS if PLURALS[p][0] == 2]: - if _locale in ['nn', 'no']: - _locale = 'nn_NO' - num_plurals = PLURALS[_locale.split('_')[0]][0] - plural_expr = PLURALS[_locale.split('_')[0]][1] - else: - num_plurals = PLURALS[_locale][0] - plural_expr = PLURALS[_locale][1] - try: - locale = Locale(_locale) - date = format_datetime(datetime.now(LOCALTZ), - 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale=_locale) - except UnknownLocaleError: - # Just an alias? Not what we're testing here, let's continue - continue - po_file = (ur"""\ -# %(english_name)s translations for TestProject. -# Copyright (C) 2007 FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , 2007. -# -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: 2007-04-01 15:30+0200\n" -"PO-Revision-Date: %(date)s\n" -"Last-Translator: FULL NAME \n" -"Language-Team: %(locale)s \n" -"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. This will be a translator comment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" - -""" % dict(locale = _locale, - english_name = locale.english_name, - version = VERSION, - year = time.strftime('%Y'), - date = date, - num_plurals = num_plurals, - plural_expr = plural_expr)).encode('utf-8') - # we should be adding the missing msgstr[0] - - # This test will fail for revisions <= 406 because so far - # catalog.num_plurals was neglected - catalog = read_po(StringIO(po_file), _locale) - message = catalog['foobar'] - checkers.num_plurals(catalog, message) - - def test_3_num_plurals_checkers(self): - for _locale in [p for p in PLURALS if PLURALS[p][0] == 3]: - po_file = r"""\ -# %(english_name)s translations for TestProject. -# Copyright (C) 2007 FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , 2007. -# -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: 2007-04-01 15:30+0200\n" -"PO-Revision-Date: %(date)s\n" -"Last-Translator: FULL NAME \n" -"Language-Team: %(locale)s \n" -"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. This will be a translator comment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" -msgstr[1] "" - -""" % dict(locale = _locale, - english_name = Locale.parse(_locale).english_name, - version = VERSION, - year = time.strftime('%Y'), - date = format_datetime(datetime.now(LOCALTZ), - 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale=_locale), - num_plurals = PLURALS[_locale][0], - plural_expr = PLURALS[_locale][0]) - - # This test will fail for revisions <= 406 because so far - # catalog.num_plurals was neglected - catalog = read_po(StringIO(po_file), _locale) - message = catalog['foobar'] - checkers.num_plurals(catalog, message) - - def test_4_num_plurals_checkers(self): - for _locale in [p for p in PLURALS if PLURALS[p][0] == 4]: - po_file = r"""\ -# %(english_name)s translations for TestProject. -# Copyright (C) 2007 FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , 2007. -# -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: 2007-04-01 15:30+0200\n" -"PO-Revision-Date: %(date)s\n" -"Last-Translator: FULL NAME \n" -"Language-Team: %(locale)s \n" -"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. This will be a translator comment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" - -""" % dict(locale = _locale, - english_name = Locale.parse(_locale).english_name, - version = VERSION, - year = time.strftime('%Y'), - date = format_datetime(datetime.now(LOCALTZ), - 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale=_locale), - num_plurals = PLURALS[_locale][0], - plural_expr = PLURALS[_locale][0]) - - # This test will fail for revisions <= 406 because so far - # catalog.num_plurals was neglected - catalog = read_po(StringIO(po_file), _locale) - message = catalog['foobar'] - checkers.num_plurals(catalog, message) - - def test_5_num_plurals_checkers(self): - for _locale in [p for p in PLURALS if PLURALS[p][0] == 5]: - po_file = r"""\ -# %(english_name)s translations for TestProject. -# Copyright (C) 2007 FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , 2007. -# -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: 2007-04-01 15:30+0200\n" -"PO-Revision-Date: %(date)s\n" -"Last-Translator: FULL NAME \n" -"Language-Team: %(locale)s \n" -"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. This will be a translator comment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" -msgstr[3] "" - -""" % dict(locale = _locale, - english_name = Locale.parse(_locale).english_name, - version = VERSION, - year = time.strftime('%Y'), - date = format_datetime(datetime.now(LOCALTZ), - 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale=_locale), - num_plurals = PLURALS[_locale][0], - plural_expr = PLURALS[_locale][0]) - - # This test will fail for revisions <= 406 because so far - # catalog.num_plurals was neglected - catalog = read_po(StringIO(po_file), _locale) - message = catalog['foobar'] - checkers.num_plurals(catalog, message) - - def test_6_num_plurals_checkers(self): - for _locale in [p for p in PLURALS if PLURALS[p][0] == 6]: - po_file = r"""\ -# %(english_name)s translations for TestProject. -# Copyright (C) 2007 FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , 2007. -# -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: 2007-04-01 15:30+0200\n" -"PO-Revision-Date: %(date)s\n" -"Last-Translator: FULL NAME \n" -"Language-Team: %(locale)s \n" -"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. This will be a translator comment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" -msgstr[3] "" -msgstr[4] "" - -""" % dict(locale = _locale, - english_name = Locale.parse(_locale).english_name, - version = VERSION, - year = time.strftime('%Y'), - date = format_datetime(datetime.now(LOCALTZ), - 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale=_locale), - num_plurals = PLURALS[_locale][0], - plural_expr = PLURALS[_locale][0]) - - # This test will fail for revisions <= 406 because so far - # catalog.num_plurals was neglected - catalog = read_po(StringIO(po_file), _locale) - message = catalog['foobar'] - checkers.num_plurals(catalog, message) - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(CheckersTestCase)) - return suite - -if __name__ == '__main__': - unittest.main(defaultTest='suite') +# -*- coding: utf-8 -*- +# +# Copyright (C) 2008 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/. + +from datetime import datetime +import time +import unittest +from StringIO import StringIO + +from babel import __version__ as VERSION +from babel.core import Locale, UnknownLocaleError +from babel.dates import format_datetime +from babel.messages import checkers +from babel.messages.plurals import PLURALS +from babel.messages.pofile import read_po +from babel.util import LOCALTZ + + +class CheckersTestCase(unittest.TestCase): + # the last msgstr[idx] is always missing except for singular plural forms + + def test_1_num_plurals_checkers(self): + for _locale in [p for p in PLURALS if PLURALS[p][0] == 1]: + try: + locale = Locale.parse(_locale) + except UnknownLocaleError: + # Just an alias? Not what we're testing here, let's continue + continue + po_file = (ur"""\ +# %(english_name)s translations for TestProject. +# Copyright (C) 2007 FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , 2007. +# +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: 2007-04-01 15:30+0200\n" +"PO-Revision-Date: %(date)s\n" +"Last-Translator: FULL NAME \n" +"Language-Team: %(locale)s \n" +"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator comment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" + +""" % dict(locale = _locale, + english_name = locale.english_name, + version = VERSION, + year = time.strftime('%Y'), + date = format_datetime(datetime.now(LOCALTZ), + 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale=_locale), + num_plurals = PLURALS[_locale][0], + plural_expr = PLURALS[_locale][0])).encode('utf-8') + + # This test will fail for revisions <= 406 because so far + # catalog.num_plurals was neglected + catalog = read_po(StringIO(po_file), _locale) + message = catalog['foobar'] + checkers.num_plurals(catalog, message) + + def test_2_num_plurals_checkers(self): + # in this testcase we add an extra msgstr[idx], we should be + # disregarding it + for _locale in [p for p in PLURALS if PLURALS[p][0] == 2]: + if _locale in ['nn', 'no']: + _locale = 'nn_NO' + num_plurals = PLURALS[_locale.split('_')[0]][0] + plural_expr = PLURALS[_locale.split('_')[0]][1] + else: + num_plurals = PLURALS[_locale][0] + plural_expr = PLURALS[_locale][1] + try: + locale = Locale(_locale) + date = format_datetime(datetime.now(LOCALTZ), + 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale=_locale) + except UnknownLocaleError: + # Just an alias? Not what we're testing here, let's continue + continue + po_file = (ur"""\ +# %(english_name)s translations for TestProject. +# Copyright (C) 2007 FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , 2007. +# +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: 2007-04-01 15:30+0200\n" +"PO-Revision-Date: %(date)s\n" +"Last-Translator: FULL NAME \n" +"Language-Team: %(locale)s \n" +"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator comment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +""" % dict(locale = _locale, + english_name = locale.english_name, + version = VERSION, + year = time.strftime('%Y'), + date = date, + num_plurals = num_plurals, + plural_expr = plural_expr)).encode('utf-8') + # we should be adding the missing msgstr[0] + + # This test will fail for revisions <= 406 because so far + # catalog.num_plurals was neglected + catalog = read_po(StringIO(po_file), _locale) + message = catalog['foobar'] + checkers.num_plurals(catalog, message) + + def test_3_num_plurals_checkers(self): + for _locale in [p for p in PLURALS if PLURALS[p][0] == 3]: + po_file = r"""\ +# %(english_name)s translations for TestProject. +# Copyright (C) 2007 FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , 2007. +# +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: 2007-04-01 15:30+0200\n" +"PO-Revision-Date: %(date)s\n" +"Last-Translator: FULL NAME \n" +"Language-Team: %(locale)s \n" +"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator comment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +""" % dict(locale = _locale, + english_name = Locale.parse(_locale).english_name, + version = VERSION, + year = time.strftime('%Y'), + date = format_datetime(datetime.now(LOCALTZ), + 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale=_locale), + num_plurals = PLURALS[_locale][0], + plural_expr = PLURALS[_locale][0]) + + # This test will fail for revisions <= 406 because so far + # catalog.num_plurals was neglected + catalog = read_po(StringIO(po_file), _locale) + message = catalog['foobar'] + checkers.num_plurals(catalog, message) + + def test_4_num_plurals_checkers(self): + for _locale in [p for p in PLURALS if PLURALS[p][0] == 4]: + po_file = r"""\ +# %(english_name)s translations for TestProject. +# Copyright (C) 2007 FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , 2007. +# +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: 2007-04-01 15:30+0200\n" +"PO-Revision-Date: %(date)s\n" +"Last-Translator: FULL NAME \n" +"Language-Team: %(locale)s \n" +"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator comment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +""" % dict(locale = _locale, + english_name = Locale.parse(_locale).english_name, + version = VERSION, + year = time.strftime('%Y'), + date = format_datetime(datetime.now(LOCALTZ), + 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale=_locale), + num_plurals = PLURALS[_locale][0], + plural_expr = PLURALS[_locale][0]) + + # This test will fail for revisions <= 406 because so far + # catalog.num_plurals was neglected + catalog = read_po(StringIO(po_file), _locale) + message = catalog['foobar'] + checkers.num_plurals(catalog, message) + + def test_5_num_plurals_checkers(self): + for _locale in [p for p in PLURALS if PLURALS[p][0] == 5]: + po_file = r"""\ +# %(english_name)s translations for TestProject. +# Copyright (C) 2007 FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , 2007. +# +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: 2007-04-01 15:30+0200\n" +"PO-Revision-Date: %(date)s\n" +"Last-Translator: FULL NAME \n" +"Language-Team: %(locale)s \n" +"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator comment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +""" % dict(locale = _locale, + english_name = Locale.parse(_locale).english_name, + version = VERSION, + year = time.strftime('%Y'), + date = format_datetime(datetime.now(LOCALTZ), + 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale=_locale), + num_plurals = PLURALS[_locale][0], + plural_expr = PLURALS[_locale][0]) + + # This test will fail for revisions <= 406 because so far + # catalog.num_plurals was neglected + catalog = read_po(StringIO(po_file), _locale) + message = catalog['foobar'] + checkers.num_plurals(catalog, message) + + def test_6_num_plurals_checkers(self): + for _locale in [p for p in PLURALS if PLURALS[p][0] == 6]: + po_file = r"""\ +# %(english_name)s translations for TestProject. +# Copyright (C) 2007 FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , 2007. +# +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: 2007-04-01 15:30+0200\n" +"PO-Revision-Date: %(date)s\n" +"Last-Translator: FULL NAME \n" +"Language-Team: %(locale)s \n" +"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator comment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" +msgstr[4] "" + +""" % dict(locale = _locale, + english_name = Locale.parse(_locale).english_name, + version = VERSION, + year = time.strftime('%Y'), + date = format_datetime(datetime.now(LOCALTZ), + 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale=_locale), + num_plurals = PLURALS[_locale][0], + plural_expr = PLURALS[_locale][0]) + + # This test will fail for revisions <= 406 because so far + # catalog.num_plurals was neglected + catalog = read_po(StringIO(po_file), _locale) + message = catalog['foobar'] + checkers.num_plurals(catalog, message) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(CheckersTestCase)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/justice/external/babel/messages/tests/data/project/file1.py b/justice/external/babel/messages/tests/data/project/file1.py index 41b71df..f957e91 100644 --- a/justice/external/babel/messages/tests/data/project/file1.py +++ b/justice/external/babel/messages/tests/data/project/file1.py @@ -1,8 +1,8 @@ -# -*- coding: utf-8 -*- -# file1.py for test - -from gettext import gettext as _ -def foo(): - # TRANSLATOR: This will be a translator coment, - # that will include several lines - print _('bar') +# -*- coding: utf-8 -*- +# file1.py for test + +from gettext import gettext as _ +def foo(): + # TRANSLATOR: This will be a translator coment, + # that will include several lines + print _('bar') diff --git a/justice/external/babel/messages/tests/data/project/file2.py b/justice/external/babel/messages/tests/data/project/file2.py index 10f556c..2585b7b 100644 --- a/justice/external/babel/messages/tests/data/project/file2.py +++ b/justice/external/babel/messages/tests/data/project/file2.py @@ -1,9 +1,9 @@ -# -*- coding: utf-8 -*- -# file2.py for test - -from gettext import ngettext - -def foo(): - # Note: This will have the TRANSLATOR: tag but shouldn't - # be included on the extracted stuff - print ngettext('foobar', 'foobars', 1) +# -*- coding: utf-8 -*- +# file2.py for test + +from gettext import ngettext + +def foo(): + # Note: This will have the TRANSLATOR: tag but shouldn't + # be included on the extracted stuff + print ngettext('foobar', 'foobars', 1) diff --git a/justice/external/babel/messages/tests/data/project/ignored/this_wont_normally_be_here.py b/justice/external/babel/messages/tests/data/project/ignored/this_wont_normally_be_here.py index ce688e3..84d607b 100644 --- a/justice/external/babel/messages/tests/data/project/ignored/this_wont_normally_be_here.py +++ b/justice/external/babel/messages/tests/data/project/ignored/this_wont_normally_be_here.py @@ -1,11 +1,11 @@ -# -*- coding: utf-8 -*- - -# This file won't normally be in this directory. -# It IS only for test - -from gettext import ngettext - -def foo(): - # Note: This will have the TRANSLATOR: tag but shouldn't - # be included on the extracted stuff - print ngettext('FooBar', 'FooBars', 1) +# -*- coding: utf-8 -*- + +# This file won't normally be in this directory. +# It IS only for test + +from gettext import ngettext + +def foo(): + # Note: This will have the TRANSLATOR: tag but shouldn't + # be included on the extracted stuff + print ngettext('FooBar', 'FooBars', 1) diff --git a/justice/external/babel/messages/tests/data/setup.py b/justice/external/babel/messages/tests/data/setup.py index f55ac3d..226ac69 100644 --- a/justice/external/babel/messages/tests/data/setup.py +++ b/justice/external/babel/messages/tests/data/setup.py @@ -1,28 +1,28 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# vim: sw=4 ts=4 fenc=utf-8 -# ============================================================================= -# $Id: setup.py 114 2007-06-14 21:17:14Z palgarvio $ -# ============================================================================= -# $URL: http://svn.edgewall.org/repos/babel/tags/0.9.6/babel/messages/test/data/setup.py $ -# $LastChangedDate: 2007-06-14 23:17:14 +0200 (do, 14 jun 2007) $ -# $Rev: 114 $ -# $LastChangedBy: palgarvio $ -# ============================================================================= -# Copyright (C) 2006 Ufsoft.org - Pedro Algarvio -# -# Please view LICENSE for additional licensing information. -# ============================================================================= - -# THIS IS A BOGUS PROJECT - -from setuptools import setup, find_packages - -setup( - name = 'TestProject', - version = '0.1', - license = 'BSD', - author = 'Foo Bar', - author_email = 'foo@bar.tld', - packages = find_packages(), -) +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# vim: sw=4 ts=4 fenc=utf-8 +# ============================================================================= +# $Id: setup.py 114 2007-06-14 21:17:14Z palgarvio $ +# ============================================================================= +# $URL: http://svn.edgewall.org/repos/babel/tags/0.9.6/babel/messages/test/data/setup.py $ +# $LastChangedDate: 2007-06-14 23:17:14 +0200 (do, 14 jun 2007) $ +# $Rev: 114 $ +# $LastChangedBy: palgarvio $ +# ============================================================================= +# Copyright (C) 2006 Ufsoft.org - Pedro Algarvio +# +# Please view LICENSE for additional licensing information. +# ============================================================================= + +# THIS IS A BOGUS PROJECT + +from setuptools import setup, find_packages + +setup( + name = 'TestProject', + version = '0.1', + license = 'BSD', + author = 'Foo Bar', + author_email = 'foo@bar.tld', + packages = find_packages(), +) diff --git a/justice/external/babel/messages/tests/extract.py b/justice/external/babel/messages/tests/extract.py index 2c1139b..6fc9056 100644 --- a/justice/external/babel/messages/tests/extract.py +++ b/justice/external/babel/messages/tests/extract.py @@ -1,533 +1,533 @@ -# -*- 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 codecs -import doctest -from StringIO import StringIO -import sys -import unittest - -from babel.messages import extract - - -class ExtractPythonTestCase(unittest.TestCase): - - def test_nested_calls(self): - buf = StringIO("""\ -msg1 = _(i18n_arg.replace(r'\"', '"')) -msg2 = ungettext(i18n_arg.replace(r'\"', '"'), multi_arg.replace(r'\"', '"'), 2) -msg3 = ungettext("Babel", multi_arg.replace(r'\"', '"'), 2) -msg4 = ungettext(i18n_arg.replace(r'\"', '"'), "Babels", 2) -msg5 = ungettext('bunny', 'bunnies', random.randint(1, 2)) -msg6 = ungettext(arg0, 'bunnies', random.randint(1, 2)) -msg7 = _(hello.there) -msg8 = gettext('Rabbit') -msg9 = dgettext('wiki', model.addPage()) -msg10 = dngettext(getDomain(), 'Page', 'Pages', 3) -""") - messages = list(extract.extract_python(buf, - extract.DEFAULT_KEYWORDS.keys(), - [], {})) - self.assertEqual([ - (1, '_', None, []), - (2, 'ungettext', (None, None, None), []), - (3, 'ungettext', (u'Babel', None, None), []), - (4, 'ungettext', (None, u'Babels', None), []), - (5, 'ungettext', (u'bunny', u'bunnies', None), []), - (6, 'ungettext', (None, u'bunnies', None), []), - (7, '_', None, []), - (8, 'gettext', u'Rabbit', []), - (9, 'dgettext', (u'wiki', None), []), - (10, 'dngettext', (None, u'Page', u'Pages', None), [])], - messages) - - def test_nested_comments(self): - buf = StringIO("""\ -msg = ngettext('pylon', # TRANSLATORS: shouldn't be - 'pylons', # TRANSLATORS: seeing this - count) -""") - messages = list(extract.extract_python(buf, ('ngettext',), - ['TRANSLATORS:'], {})) - self.assertEqual([(1, 'ngettext', (u'pylon', u'pylons', None), [])], - messages) - - def test_comments_with_calls_that_spawn_multiple_lines(self): - buf = StringIO("""\ -# NOTE: This Comment SHOULD Be Extracted -add_notice(req, ngettext("Catalog deleted.", - "Catalogs deleted.", len(selected))) - -# NOTE: This Comment SHOULD Be Extracted -add_notice(req, _("Locale deleted.")) - - -# NOTE: This Comment SHOULD Be Extracted -add_notice(req, ngettext("Foo deleted.", "Foos deleted.", len(selected))) - -# NOTE: This Comment SHOULD Be Extracted -# NOTE: And This One Too -add_notice(req, ngettext("Bar deleted.", - "Bars deleted.", len(selected))) -""") - messages = list(extract.extract_python(buf, ('ngettext','_'), ['NOTE:'], - - {'strip_comment_tags':False})) - self.assertEqual((6, '_', 'Locale deleted.', - [u'NOTE: This Comment SHOULD Be Extracted']), - messages[1]) - self.assertEqual((10, 'ngettext', (u'Foo deleted.', u'Foos deleted.', - None), - [u'NOTE: This Comment SHOULD Be Extracted']), - messages[2]) - self.assertEqual((3, 'ngettext', - (u'Catalog deleted.', - u'Catalogs deleted.', None), - [u'NOTE: This Comment SHOULD Be Extracted']), - messages[0]) - self.assertEqual((15, 'ngettext', (u'Bar deleted.', u'Bars deleted.', - None), - [u'NOTE: This Comment SHOULD Be Extracted', - u'NOTE: And This One Too']), - messages[3]) - - def test_declarations(self): - buf = StringIO("""\ -class gettext(object): - pass -def render_body(context,x,y=_('Page arg 1'),z=_('Page arg 2'),**pageargs): - pass -def ngettext(y='arg 1',z='arg 2',**pageargs): - pass -class Meta: - verbose_name = _('log entry') -""") - messages = list(extract.extract_python(buf, - extract.DEFAULT_KEYWORDS.keys(), - [], {})) - self.assertEqual([(3, '_', u'Page arg 1', []), - (3, '_', u'Page arg 2', []), - (8, '_', u'log entry', [])], - messages) - - def test_multiline(self): - buf = StringIO("""\ -msg1 = ngettext('pylon', - 'pylons', count) -msg2 = ngettext('elvis', - 'elvises', - count) -""") - messages = list(extract.extract_python(buf, ('ngettext',), [], {})) - self.assertEqual([(1, 'ngettext', (u'pylon', u'pylons', None), []), - (3, 'ngettext', (u'elvis', u'elvises', None), [])], - messages) - - def test_triple_quoted_strings(self): - buf = StringIO("""\ -msg1 = _('''pylons''') -msg2 = ngettext(r'''elvis''', \"\"\"elvises\"\"\", count) -msg2 = ngettext(\"\"\"elvis\"\"\", 'elvises', count) -""") - messages = list(extract.extract_python(buf, - extract.DEFAULT_KEYWORDS.keys(), - [], {})) - self.assertEqual([(1, '_', (u'pylons'), []), - (2, 'ngettext', (u'elvis', u'elvises', None), []), - (3, 'ngettext', (u'elvis', u'elvises', None), [])], - messages) - - def test_multiline_strings(self): - buf = StringIO("""\ -_('''This module provides internationalization and localization -support for your Python programs by providing an interface to the GNU -gettext message catalog library.''') -""") - messages = list(extract.extract_python(buf, - extract.DEFAULT_KEYWORDS.keys(), - [], {})) - self.assertEqual( - [(1, '_', - u'This module provides internationalization and localization\n' - 'support for your Python programs by providing an interface to ' - 'the GNU\ngettext message catalog library.', [])], - messages) - - def test_concatenated_strings(self): - buf = StringIO("""\ -foobar = _('foo' 'bar') -""") - messages = list(extract.extract_python(buf, - extract.DEFAULT_KEYWORDS.keys(), - [], {})) - self.assertEqual(u'foobar', messages[0][2]) - - def test_unicode_string_arg(self): - buf = StringIO("msg = _(u'Foo Bar')") - messages = list(extract.extract_python(buf, ('_',), [], {})) - self.assertEqual(u'Foo Bar', messages[0][2]) - - def test_comment_tag(self): - buf = StringIO(""" -# NOTE: A translation comment -msg = _(u'Foo Bar') -""") - messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) - self.assertEqual(u'Foo Bar', messages[0][2]) - self.assertEqual([u'NOTE: A translation comment'], messages[0][3]) - - def test_comment_tag_multiline(self): - buf = StringIO(""" -# NOTE: A translation comment -# with a second line -msg = _(u'Foo Bar') -""") - messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) - self.assertEqual(u'Foo Bar', messages[0][2]) - self.assertEqual([u'NOTE: A translation comment', u'with a second line'], - messages[0][3]) - - def test_translator_comments_with_previous_non_translator_comments(self): - buf = StringIO(""" -# This shouldn't be in the output -# because it didn't start with a comment tag -# NOTE: A translation comment -# with a second line -msg = _(u'Foo Bar') -""") - messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) - self.assertEqual(u'Foo Bar', messages[0][2]) - self.assertEqual([u'NOTE: A translation comment', u'with a second line'], - messages[0][3]) - - def test_comment_tags_not_on_start_of_comment(self): - buf = StringIO(""" -# This shouldn't be in the output -# because it didn't start with a comment tag -# do NOTE: this will not be a translation comment -# NOTE: This one will be -msg = _(u'Foo Bar') -""") - messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) - self.assertEqual(u'Foo Bar', messages[0][2]) - self.assertEqual([u'NOTE: This one will be'], messages[0][3]) - - def test_multiple_comment_tags(self): - buf = StringIO(""" -# NOTE1: A translation comment for tag1 -# with a second line -msg = _(u'Foo Bar1') - -# NOTE2: A translation comment for tag2 -msg = _(u'Foo Bar2') -""") - messages = list(extract.extract_python(buf, ('_',), - ['NOTE1:', 'NOTE2:'], {})) - self.assertEqual(u'Foo Bar1', messages[0][2]) - self.assertEqual([u'NOTE1: A translation comment for tag1', - u'with a second line'], messages[0][3]) - self.assertEqual(u'Foo Bar2', messages[1][2]) - self.assertEqual([u'NOTE2: A translation comment for tag2'], messages[1][3]) - - def test_two_succeeding_comments(self): - buf = StringIO(""" -# NOTE: one -# NOTE: two -msg = _(u'Foo Bar') -""") - messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) - self.assertEqual(u'Foo Bar', messages[0][2]) - self.assertEqual([u'NOTE: one', u'NOTE: two'], messages[0][3]) - - def test_invalid_translator_comments(self): - buf = StringIO(""" -# NOTE: this shouldn't apply to any messages -hello = 'there' - -msg = _(u'Foo Bar') -""") - messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) - self.assertEqual(u'Foo Bar', messages[0][2]) - self.assertEqual([], messages[0][3]) - - def test_invalid_translator_comments2(self): - buf = StringIO(""" -# NOTE: Hi! -hithere = _('Hi there!') - -# NOTE: you should not be seeing this in the .po -rows = [[v for v in range(0,10)] for row in range(0,10)] - -# this (NOTE:) should not show up either -hello = _('Hello') -""") - messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) - self.assertEqual(u'Hi there!', messages[0][2]) - self.assertEqual([u'NOTE: Hi!'], messages[0][3]) - self.assertEqual(u'Hello', messages[1][2]) - self.assertEqual([], messages[1][3]) - - def test_invalid_translator_comments3(self): - buf = StringIO(""" -# NOTE: Hi, - -# there! -hithere = _('Hi there!') -""") - messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) - self.assertEqual(u'Hi there!', messages[0][2]) - self.assertEqual([], messages[0][3]) - - def test_comment_tag_with_leading_space(self): - buf = StringIO(""" - #: A translation comment - #: with leading spaces -msg = _(u'Foo Bar') -""") - messages = list(extract.extract_python(buf, ('_',), [':'], {})) - self.assertEqual(u'Foo Bar', messages[0][2]) - self.assertEqual([u': A translation comment', u': with leading spaces'], - messages[0][3]) - - def test_different_signatures(self): - buf = StringIO(""" -foo = _('foo', 'bar') -n = ngettext('hello', 'there', n=3) -n = ngettext(n=3, 'hello', 'there') -n = ngettext(n=3, *messages) -n = ngettext() -n = ngettext('foo') -""") - messages = list(extract.extract_python(buf, ('_', 'ngettext'), [], {})) - self.assertEqual((u'foo', u'bar'), messages[0][2]) - self.assertEqual((u'hello', u'there', None), messages[1][2]) - self.assertEqual((None, u'hello', u'there'), messages[2][2]) - self.assertEqual((None, None), messages[3][2]) - self.assertEqual(None, messages[4][2]) - self.assertEqual(('foo'), messages[5][2]) - - def test_utf8_message(self): - buf = StringIO(""" -# NOTE: hello -msg = _('Bonjour à tous') -""") - messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], - {'encoding': 'utf-8'})) - self.assertEqual(u'Bonjour à tous', messages[0][2]) - self.assertEqual([u'NOTE: hello'], messages[0][3]) - - def test_utf8_message_with_magic_comment(self): - buf = StringIO("""# -*- coding: utf-8 -*- -# NOTE: hello -msg = _('Bonjour à tous') -""") - messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) - self.assertEqual(u'Bonjour à tous', messages[0][2]) - self.assertEqual([u'NOTE: hello'], messages[0][3]) - - def test_utf8_message_with_utf8_bom(self): - buf = StringIO(codecs.BOM_UTF8 + """ -# NOTE: hello -msg = _('Bonjour à tous') -""") - messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) - self.assertEqual(u'Bonjour à tous', messages[0][2]) - self.assertEqual([u'NOTE: hello'], messages[0][3]) - - def test_utf8_raw_strings_match_unicode_strings(self): - buf = StringIO(codecs.BOM_UTF8 + """ -msg = _('Bonjour à tous') -msgu = _(u'Bonjour à tous') -""") - messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) - self.assertEqual(u'Bonjour à tous', messages[0][2]) - self.assertEqual(messages[0][2], messages[1][2]) - - def test_extract_strip_comment_tags(self): - buf = StringIO("""\ -#: This is a comment with a very simple -#: prefix specified -_('Servus') - -# NOTE: This is a multiline comment with -# a prefix too -_('Babatschi')""") - messages = list(extract.extract('python', buf, comment_tags=['NOTE:', ':'], - strip_comment_tags=True)) - self.assertEqual(u'Servus', messages[0][1]) - self.assertEqual([u'This is a comment with a very simple', - u'prefix specified'], messages[0][2]) - self.assertEqual(u'Babatschi', messages[1][1]) - self.assertEqual([u'This is a multiline comment with', - u'a prefix too'], messages[1][2]) - - -class ExtractJavaScriptTestCase(unittest.TestCase): - - def test_simple_extract(self): - buf = StringIO("""\ -msg1 = _('simple') -msg2 = gettext('simple') -msg3 = ngettext('s', 'p', 42) - """) - messages = \ - list(extract.extract('javascript', buf, extract.DEFAULT_KEYWORDS, - [], {})) - - self.assertEqual([(1, 'simple', []), - (2, 'simple', []), - (3, ('s', 'p'), [])], messages) - - def test_various_calls(self): - buf = StringIO("""\ -msg1 = _(i18n_arg.replace(/"/, '"')) -msg2 = ungettext(i18n_arg.replace(/"/, '"'), multi_arg.replace(/"/, '"'), 2) -msg3 = ungettext("Babel", multi_arg.replace(/"/, '"'), 2) -msg4 = ungettext(i18n_arg.replace(/"/, '"'), "Babels", 2) -msg5 = ungettext('bunny', 'bunnies', parseInt(Math.random() * 2 + 1)) -msg6 = ungettext(arg0, 'bunnies', rparseInt(Math.random() * 2 + 1)) -msg7 = _(hello.there) -msg8 = gettext('Rabbit') -msg9 = dgettext('wiki', model.addPage()) -msg10 = dngettext(domain, 'Page', 'Pages', 3) -""") - messages = \ - list(extract.extract('javascript', buf, extract.DEFAULT_KEYWORDS, [], - {})) - self.assertEqual([(5, (u'bunny', u'bunnies'), []), - (8, u'Rabbit', []), - (10, (u'Page', u'Pages'), [])], messages) - - def test_message_with_line_comment(self): - buf = StringIO("""\ -// NOTE: hello -msg = _('Bonjour à tous') -""") - messages = list(extract.extract_javascript(buf, ('_',), ['NOTE:'], {})) - self.assertEqual(u'Bonjour à tous', messages[0][2]) - self.assertEqual([u'NOTE: hello'], messages[0][3]) - - def test_message_with_multiline_comment(self): - buf = StringIO("""\ -/* NOTE: hello - and bonjour - and servus */ -msg = _('Bonjour à tous') -""") - messages = list(extract.extract_javascript(buf, ('_',), ['NOTE:'], {})) - self.assertEqual(u'Bonjour à tous', messages[0][2]) - self.assertEqual([u'NOTE: hello', 'and bonjour', ' and servus'], messages[0][3]) - - def test_ignore_function_definitions(self): - buf = StringIO("""\ -function gettext(value) { - return translations[language][value] || value; -}""") - - messages = list(extract.extract_javascript(buf, ('gettext',), [], {})) - self.assertEqual(messages, []) - - def test_misplaced_comments(self): - buf = StringIO("""\ -/* NOTE: this won't show up */ -foo() - -/* NOTE: this will */ -msg = _('Something') - -// NOTE: this will show up -// too. -msg = _('Something else') - -// NOTE: but this won't -bar() - -_('no comment here') -""") - messages = list(extract.extract_javascript(buf, ('_',), ['NOTE:'], {})) - self.assertEqual(u'Something', messages[0][2]) - self.assertEqual([u'NOTE: this will'], messages[0][3]) - self.assertEqual(u'Something else', messages[1][2]) - self.assertEqual([u'NOTE: this will show up', 'too.'], messages[1][3]) - self.assertEqual(u'no comment here', messages[2][2]) - self.assertEqual([], messages[2][3]) - - -class ExtractTestCase(unittest.TestCase): - - def test_invalid_filter(self): - buf = StringIO("""\ -msg1 = _(i18n_arg.replace(r'\"', '"')) -msg2 = ungettext(i18n_arg.replace(r'\"', '"'), multi_arg.replace(r'\"', '"'), 2) -msg3 = ungettext("Babel", multi_arg.replace(r'\"', '"'), 2) -msg4 = ungettext(i18n_arg.replace(r'\"', '"'), "Babels", 2) -msg5 = ungettext('bunny', 'bunnies', random.randint(1, 2)) -msg6 = ungettext(arg0, 'bunnies', random.randint(1, 2)) -msg7 = _(hello.there) -msg8 = gettext('Rabbit') -msg9 = dgettext('wiki', model.addPage()) -msg10 = dngettext(domain, 'Page', 'Pages', 3) -""") - messages = \ - list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [], - {})) - self.assertEqual([(5, (u'bunny', u'bunnies'), []), - (8, u'Rabbit', []), - (10, (u'Page', u'Pages'), [])], messages) - - def test_invalid_extract_method(self): - buf = StringIO('') - self.assertRaises(ValueError, list, extract.extract('spam', buf)) - - def test_different_signatures(self): - buf = StringIO(""" -foo = _('foo', 'bar') -n = ngettext('hello', 'there', n=3) -n = ngettext(n=3, 'hello', 'there') -n = ngettext(n=3, *messages) -n = ngettext() -n = ngettext('foo') -""") - messages = \ - list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [], - {})) - self.assertEqual(len(messages), 2) - self.assertEqual(u'foo', messages[0][1]) - self.assertEqual((u'hello', u'there'), messages[1][1]) - - def test_empty_string_msgid(self): - buf = StringIO("""\ -msg = _('') -""") - stderr = sys.stderr - sys.stderr = StringIO() - try: - messages = \ - list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, - [], {})) - self.assertEqual([], messages) - assert 'warning: Empty msgid.' in sys.stderr.getvalue() - finally: - sys.stderr = stderr - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(extract)) - suite.addTest(unittest.makeSuite(ExtractPythonTestCase)) - suite.addTest(unittest.makeSuite(ExtractJavaScriptTestCase)) - suite.addTest(unittest.makeSuite(ExtractTestCase)) - return suite - -if __name__ == '__main__': - unittest.main(defaultTest='suite') +# -*- 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 codecs +import doctest +from StringIO import StringIO +import sys +import unittest + +from babel.messages import extract + + +class ExtractPythonTestCase(unittest.TestCase): + + def test_nested_calls(self): + buf = StringIO("""\ +msg1 = _(i18n_arg.replace(r'\"', '"')) +msg2 = ungettext(i18n_arg.replace(r'\"', '"'), multi_arg.replace(r'\"', '"'), 2) +msg3 = ungettext("Babel", multi_arg.replace(r'\"', '"'), 2) +msg4 = ungettext(i18n_arg.replace(r'\"', '"'), "Babels", 2) +msg5 = ungettext('bunny', 'bunnies', random.randint(1, 2)) +msg6 = ungettext(arg0, 'bunnies', random.randint(1, 2)) +msg7 = _(hello.there) +msg8 = gettext('Rabbit') +msg9 = dgettext('wiki', model.addPage()) +msg10 = dngettext(getDomain(), 'Page', 'Pages', 3) +""") + messages = list(extract.extract_python(buf, + extract.DEFAULT_KEYWORDS.keys(), + [], {})) + self.assertEqual([ + (1, '_', None, []), + (2, 'ungettext', (None, None, None), []), + (3, 'ungettext', (u'Babel', None, None), []), + (4, 'ungettext', (None, u'Babels', None), []), + (5, 'ungettext', (u'bunny', u'bunnies', None), []), + (6, 'ungettext', (None, u'bunnies', None), []), + (7, '_', None, []), + (8, 'gettext', u'Rabbit', []), + (9, 'dgettext', (u'wiki', None), []), + (10, 'dngettext', (None, u'Page', u'Pages', None), [])], + messages) + + def test_nested_comments(self): + buf = StringIO("""\ +msg = ngettext('pylon', # TRANSLATORS: shouldn't be + 'pylons', # TRANSLATORS: seeing this + count) +""") + messages = list(extract.extract_python(buf, ('ngettext',), + ['TRANSLATORS:'], {})) + self.assertEqual([(1, 'ngettext', (u'pylon', u'pylons', None), [])], + messages) + + def test_comments_with_calls_that_spawn_multiple_lines(self): + buf = StringIO("""\ +# NOTE: This Comment SHOULD Be Extracted +add_notice(req, ngettext("Catalog deleted.", + "Catalogs deleted.", len(selected))) + +# NOTE: This Comment SHOULD Be Extracted +add_notice(req, _("Locale deleted.")) + + +# NOTE: This Comment SHOULD Be Extracted +add_notice(req, ngettext("Foo deleted.", "Foos deleted.", len(selected))) + +# NOTE: This Comment SHOULD Be Extracted +# NOTE: And This One Too +add_notice(req, ngettext("Bar deleted.", + "Bars deleted.", len(selected))) +""") + messages = list(extract.extract_python(buf, ('ngettext','_'), ['NOTE:'], + + {'strip_comment_tags':False})) + self.assertEqual((6, '_', 'Locale deleted.', + [u'NOTE: This Comment SHOULD Be Extracted']), + messages[1]) + self.assertEqual((10, 'ngettext', (u'Foo deleted.', u'Foos deleted.', + None), + [u'NOTE: This Comment SHOULD Be Extracted']), + messages[2]) + self.assertEqual((3, 'ngettext', + (u'Catalog deleted.', + u'Catalogs deleted.', None), + [u'NOTE: This Comment SHOULD Be Extracted']), + messages[0]) + self.assertEqual((15, 'ngettext', (u'Bar deleted.', u'Bars deleted.', + None), + [u'NOTE: This Comment SHOULD Be Extracted', + u'NOTE: And This One Too']), + messages[3]) + + def test_declarations(self): + buf = StringIO("""\ +class gettext(object): + pass +def render_body(context,x,y=_('Page arg 1'),z=_('Page arg 2'),**pageargs): + pass +def ngettext(y='arg 1',z='arg 2',**pageargs): + pass +class Meta: + verbose_name = _('log entry') +""") + messages = list(extract.extract_python(buf, + extract.DEFAULT_KEYWORDS.keys(), + [], {})) + self.assertEqual([(3, '_', u'Page arg 1', []), + (3, '_', u'Page arg 2', []), + (8, '_', u'log entry', [])], + messages) + + def test_multiline(self): + buf = StringIO("""\ +msg1 = ngettext('pylon', + 'pylons', count) +msg2 = ngettext('elvis', + 'elvises', + count) +""") + messages = list(extract.extract_python(buf, ('ngettext',), [], {})) + self.assertEqual([(1, 'ngettext', (u'pylon', u'pylons', None), []), + (3, 'ngettext', (u'elvis', u'elvises', None), [])], + messages) + + def test_triple_quoted_strings(self): + buf = StringIO("""\ +msg1 = _('''pylons''') +msg2 = ngettext(r'''elvis''', \"\"\"elvises\"\"\", count) +msg2 = ngettext(\"\"\"elvis\"\"\", 'elvises', count) +""") + messages = list(extract.extract_python(buf, + extract.DEFAULT_KEYWORDS.keys(), + [], {})) + self.assertEqual([(1, '_', (u'pylons'), []), + (2, 'ngettext', (u'elvis', u'elvises', None), []), + (3, 'ngettext', (u'elvis', u'elvises', None), [])], + messages) + + def test_multiline_strings(self): + buf = StringIO("""\ +_('''This module provides internationalization and localization +support for your Python programs by providing an interface to the GNU +gettext message catalog library.''') +""") + messages = list(extract.extract_python(buf, + extract.DEFAULT_KEYWORDS.keys(), + [], {})) + self.assertEqual( + [(1, '_', + u'This module provides internationalization and localization\n' + 'support for your Python programs by providing an interface to ' + 'the GNU\ngettext message catalog library.', [])], + messages) + + def test_concatenated_strings(self): + buf = StringIO("""\ +foobar = _('foo' 'bar') +""") + messages = list(extract.extract_python(buf, + extract.DEFAULT_KEYWORDS.keys(), + [], {})) + self.assertEqual(u'foobar', messages[0][2]) + + def test_unicode_string_arg(self): + buf = StringIO("msg = _(u'Foo Bar')") + messages = list(extract.extract_python(buf, ('_',), [], {})) + self.assertEqual(u'Foo Bar', messages[0][2]) + + def test_comment_tag(self): + buf = StringIO(""" +# NOTE: A translation comment +msg = _(u'Foo Bar') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual(u'Foo Bar', messages[0][2]) + self.assertEqual([u'NOTE: A translation comment'], messages[0][3]) + + def test_comment_tag_multiline(self): + buf = StringIO(""" +# NOTE: A translation comment +# with a second line +msg = _(u'Foo Bar') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual(u'Foo Bar', messages[0][2]) + self.assertEqual([u'NOTE: A translation comment', u'with a second line'], + messages[0][3]) + + def test_translator_comments_with_previous_non_translator_comments(self): + buf = StringIO(""" +# This shouldn't be in the output +# because it didn't start with a comment tag +# NOTE: A translation comment +# with a second line +msg = _(u'Foo Bar') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual(u'Foo Bar', messages[0][2]) + self.assertEqual([u'NOTE: A translation comment', u'with a second line'], + messages[0][3]) + + def test_comment_tags_not_on_start_of_comment(self): + buf = StringIO(""" +# This shouldn't be in the output +# because it didn't start with a comment tag +# do NOTE: this will not be a translation comment +# NOTE: This one will be +msg = _(u'Foo Bar') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual(u'Foo Bar', messages[0][2]) + self.assertEqual([u'NOTE: This one will be'], messages[0][3]) + + def test_multiple_comment_tags(self): + buf = StringIO(""" +# NOTE1: A translation comment for tag1 +# with a second line +msg = _(u'Foo Bar1') + +# NOTE2: A translation comment for tag2 +msg = _(u'Foo Bar2') +""") + messages = list(extract.extract_python(buf, ('_',), + ['NOTE1:', 'NOTE2:'], {})) + self.assertEqual(u'Foo Bar1', messages[0][2]) + self.assertEqual([u'NOTE1: A translation comment for tag1', + u'with a second line'], messages[0][3]) + self.assertEqual(u'Foo Bar2', messages[1][2]) + self.assertEqual([u'NOTE2: A translation comment for tag2'], messages[1][3]) + + def test_two_succeeding_comments(self): + buf = StringIO(""" +# NOTE: one +# NOTE: two +msg = _(u'Foo Bar') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual(u'Foo Bar', messages[0][2]) + self.assertEqual([u'NOTE: one', u'NOTE: two'], messages[0][3]) + + def test_invalid_translator_comments(self): + buf = StringIO(""" +# NOTE: this shouldn't apply to any messages +hello = 'there' + +msg = _(u'Foo Bar') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual(u'Foo Bar', messages[0][2]) + self.assertEqual([], messages[0][3]) + + def test_invalid_translator_comments2(self): + buf = StringIO(""" +# NOTE: Hi! +hithere = _('Hi there!') + +# NOTE: you should not be seeing this in the .po +rows = [[v for v in range(0,10)] for row in range(0,10)] + +# this (NOTE:) should not show up either +hello = _('Hello') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual(u'Hi there!', messages[0][2]) + self.assertEqual([u'NOTE: Hi!'], messages[0][3]) + self.assertEqual(u'Hello', messages[1][2]) + self.assertEqual([], messages[1][3]) + + def test_invalid_translator_comments3(self): + buf = StringIO(""" +# NOTE: Hi, + +# there! +hithere = _('Hi there!') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual(u'Hi there!', messages[0][2]) + self.assertEqual([], messages[0][3]) + + def test_comment_tag_with_leading_space(self): + buf = StringIO(""" + #: A translation comment + #: with leading spaces +msg = _(u'Foo Bar') +""") + messages = list(extract.extract_python(buf, ('_',), [':'], {})) + self.assertEqual(u'Foo Bar', messages[0][2]) + self.assertEqual([u': A translation comment', u': with leading spaces'], + messages[0][3]) + + def test_different_signatures(self): + buf = StringIO(""" +foo = _('foo', 'bar') +n = ngettext('hello', 'there', n=3) +n = ngettext(n=3, 'hello', 'there') +n = ngettext(n=3, *messages) +n = ngettext() +n = ngettext('foo') +""") + messages = list(extract.extract_python(buf, ('_', 'ngettext'), [], {})) + self.assertEqual((u'foo', u'bar'), messages[0][2]) + self.assertEqual((u'hello', u'there', None), messages[1][2]) + self.assertEqual((None, u'hello', u'there'), messages[2][2]) + self.assertEqual((None, None), messages[3][2]) + self.assertEqual(None, messages[4][2]) + self.assertEqual(('foo'), messages[5][2]) + + def test_utf8_message(self): + buf = StringIO(""" +# NOTE: hello +msg = _('Bonjour à tous') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], + {'encoding': 'utf-8'})) + self.assertEqual(u'Bonjour à tous', messages[0][2]) + self.assertEqual([u'NOTE: hello'], messages[0][3]) + + def test_utf8_message_with_magic_comment(self): + buf = StringIO("""# -*- coding: utf-8 -*- +# NOTE: hello +msg = _('Bonjour à tous') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual(u'Bonjour à tous', messages[0][2]) + self.assertEqual([u'NOTE: hello'], messages[0][3]) + + def test_utf8_message_with_utf8_bom(self): + buf = StringIO(codecs.BOM_UTF8 + """ +# NOTE: hello +msg = _('Bonjour à tous') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual(u'Bonjour à tous', messages[0][2]) + self.assertEqual([u'NOTE: hello'], messages[0][3]) + + def test_utf8_raw_strings_match_unicode_strings(self): + buf = StringIO(codecs.BOM_UTF8 + """ +msg = _('Bonjour à tous') +msgu = _(u'Bonjour à tous') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual(u'Bonjour à tous', messages[0][2]) + self.assertEqual(messages[0][2], messages[1][2]) + + def test_extract_strip_comment_tags(self): + buf = StringIO("""\ +#: This is a comment with a very simple +#: prefix specified +_('Servus') + +# NOTE: This is a multiline comment with +# a prefix too +_('Babatschi')""") + messages = list(extract.extract('python', buf, comment_tags=['NOTE:', ':'], + strip_comment_tags=True)) + self.assertEqual(u'Servus', messages[0][1]) + self.assertEqual([u'This is a comment with a very simple', + u'prefix specified'], messages[0][2]) + self.assertEqual(u'Babatschi', messages[1][1]) + self.assertEqual([u'This is a multiline comment with', + u'a prefix too'], messages[1][2]) + + +class ExtractJavaScriptTestCase(unittest.TestCase): + + def test_simple_extract(self): + buf = StringIO("""\ +msg1 = _('simple') +msg2 = gettext('simple') +msg3 = ngettext('s', 'p', 42) + """) + messages = \ + list(extract.extract('javascript', buf, extract.DEFAULT_KEYWORDS, + [], {})) + + self.assertEqual([(1, 'simple', []), + (2, 'simple', []), + (3, ('s', 'p'), [])], messages) + + def test_various_calls(self): + buf = StringIO("""\ +msg1 = _(i18n_arg.replace(/"/, '"')) +msg2 = ungettext(i18n_arg.replace(/"/, '"'), multi_arg.replace(/"/, '"'), 2) +msg3 = ungettext("Babel", multi_arg.replace(/"/, '"'), 2) +msg4 = ungettext(i18n_arg.replace(/"/, '"'), "Babels", 2) +msg5 = ungettext('bunny', 'bunnies', parseInt(Math.random() * 2 + 1)) +msg6 = ungettext(arg0, 'bunnies', rparseInt(Math.random() * 2 + 1)) +msg7 = _(hello.there) +msg8 = gettext('Rabbit') +msg9 = dgettext('wiki', model.addPage()) +msg10 = dngettext(domain, 'Page', 'Pages', 3) +""") + messages = \ + list(extract.extract('javascript', buf, extract.DEFAULT_KEYWORDS, [], + {})) + self.assertEqual([(5, (u'bunny', u'bunnies'), []), + (8, u'Rabbit', []), + (10, (u'Page', u'Pages'), [])], messages) + + def test_message_with_line_comment(self): + buf = StringIO("""\ +// NOTE: hello +msg = _('Bonjour à tous') +""") + messages = list(extract.extract_javascript(buf, ('_',), ['NOTE:'], {})) + self.assertEqual(u'Bonjour à tous', messages[0][2]) + self.assertEqual([u'NOTE: hello'], messages[0][3]) + + def test_message_with_multiline_comment(self): + buf = StringIO("""\ +/* NOTE: hello + and bonjour + and servus */ +msg = _('Bonjour à tous') +""") + messages = list(extract.extract_javascript(buf, ('_',), ['NOTE:'], {})) + self.assertEqual(u'Bonjour à tous', messages[0][2]) + self.assertEqual([u'NOTE: hello', 'and bonjour', ' and servus'], messages[0][3]) + + def test_ignore_function_definitions(self): + buf = StringIO("""\ +function gettext(value) { + return translations[language][value] || value; +}""") + + messages = list(extract.extract_javascript(buf, ('gettext',), [], {})) + self.assertEqual(messages, []) + + def test_misplaced_comments(self): + buf = StringIO("""\ +/* NOTE: this won't show up */ +foo() + +/* NOTE: this will */ +msg = _('Something') + +// NOTE: this will show up +// too. +msg = _('Something else') + +// NOTE: but this won't +bar() + +_('no comment here') +""") + messages = list(extract.extract_javascript(buf, ('_',), ['NOTE:'], {})) + self.assertEqual(u'Something', messages[0][2]) + self.assertEqual([u'NOTE: this will'], messages[0][3]) + self.assertEqual(u'Something else', messages[1][2]) + self.assertEqual([u'NOTE: this will show up', 'too.'], messages[1][3]) + self.assertEqual(u'no comment here', messages[2][2]) + self.assertEqual([], messages[2][3]) + + +class ExtractTestCase(unittest.TestCase): + + def test_invalid_filter(self): + buf = StringIO("""\ +msg1 = _(i18n_arg.replace(r'\"', '"')) +msg2 = ungettext(i18n_arg.replace(r'\"', '"'), multi_arg.replace(r'\"', '"'), 2) +msg3 = ungettext("Babel", multi_arg.replace(r'\"', '"'), 2) +msg4 = ungettext(i18n_arg.replace(r'\"', '"'), "Babels", 2) +msg5 = ungettext('bunny', 'bunnies', random.randint(1, 2)) +msg6 = ungettext(arg0, 'bunnies', random.randint(1, 2)) +msg7 = _(hello.there) +msg8 = gettext('Rabbit') +msg9 = dgettext('wiki', model.addPage()) +msg10 = dngettext(domain, 'Page', 'Pages', 3) +""") + messages = \ + list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [], + {})) + self.assertEqual([(5, (u'bunny', u'bunnies'), []), + (8, u'Rabbit', []), + (10, (u'Page', u'Pages'), [])], messages) + + def test_invalid_extract_method(self): + buf = StringIO('') + self.assertRaises(ValueError, list, extract.extract('spam', buf)) + + def test_different_signatures(self): + buf = StringIO(""" +foo = _('foo', 'bar') +n = ngettext('hello', 'there', n=3) +n = ngettext(n=3, 'hello', 'there') +n = ngettext(n=3, *messages) +n = ngettext() +n = ngettext('foo') +""") + messages = \ + list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [], + {})) + self.assertEqual(len(messages), 2) + self.assertEqual(u'foo', messages[0][1]) + self.assertEqual((u'hello', u'there'), messages[1][1]) + + def test_empty_string_msgid(self): + buf = StringIO("""\ +msg = _('') +""") + stderr = sys.stderr + sys.stderr = StringIO() + try: + messages = \ + list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, + [], {})) + self.assertEqual([], messages) + assert 'warning: Empty msgid.' in sys.stderr.getvalue() + finally: + sys.stderr = stderr + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(extract)) + suite.addTest(unittest.makeSuite(ExtractPythonTestCase)) + suite.addTest(unittest.makeSuite(ExtractJavaScriptTestCase)) + suite.addTest(unittest.makeSuite(ExtractTestCase)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/justice/external/babel/messages/tests/frontend.py b/justice/external/babel/messages/tests/frontend.py index aabe487..c626971 100644 --- a/justice/external/babel/messages/tests/frontend.py +++ b/justice/external/babel/messages/tests/frontend.py @@ -1,906 +1,906 @@ -# -*- 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/. - -from datetime import datetime -from distutils.dist import Distribution -from distutils.errors import DistutilsOptionError -from distutils.log import _global_log -import doctest -import logging -import os -import shutil -from StringIO import StringIO -import sys -import time -import unittest - -from babel import __version__ as VERSION -from babel.dates import format_datetime -from babel.messages import frontend -from babel.util import LOCALTZ - - -class CompileCatalogTestCase(unittest.TestCase): - - def setUp(self): - self.olddir = os.getcwd() - self.datadir = os.path.join(os.path.dirname(__file__), 'data') - os.chdir(self.datadir) - _global_log.threshold = 5 # shut up distutils logging - - self.dist = Distribution(dict( - name='TestProject', - version='0.1', - packages=['project'] - )) - self.cmd = frontend.compile_catalog(self.dist) - self.cmd.initialize_options() - - def tearDown(self): - os.chdir(self.olddir) - - def test_no_directory_or_output_file_specified(self): - self.cmd.locale = 'en_US' - self.cmd.input_file = 'dummy' - self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) - - def test_no_directory_or_input_file_specified(self): - self.cmd.locale = 'en_US' - self.cmd.output_file = 'dummy' - self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) - - -class ExtractMessagesTestCase(unittest.TestCase): - - def setUp(self): - self.olddir = os.getcwd() - self.datadir = os.path.join(os.path.dirname(__file__), 'data') - os.chdir(self.datadir) - _global_log.threshold = 5 # shut up distutils logging - - self.dist = Distribution(dict( - name='TestProject', - version='0.1', - packages=['project'] - )) - self.cmd = frontend.extract_messages(self.dist) - self.cmd.initialize_options() - - def tearDown(self): - pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') - if os.path.isfile(pot_file): - os.unlink(pot_file) - - os.chdir(self.olddir) - - def test_neither_default_nor_custom_keywords(self): - self.cmd.output_file = 'dummy' - self.cmd.no_default_keywords = True - self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) - - def test_no_output_file_specified(self): - self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) - - def test_both_sort_output_and_sort_by_file(self): - self.cmd.output_file = 'dummy' - self.cmd.sort_output = True - self.cmd.sort_by_file = True - self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) - - def test_extraction_with_default_mapping(self): - self.cmd.copyright_holder = 'FooBar, Inc.' - self.cmd.msgid_bugs_address = 'bugs.address@email.tld' - self.cmd.output_file = 'project/i18n/temp.pot' - self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:' - - self.cmd.finalize_options() - self.cmd.run() - - pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') - assert os.path.isfile(pot_file) - - self.assertEqual( -r"""# Translations template for TestProject. -# Copyright (C) %(year)s FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , %(year)s. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: %(date)s\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. TRANSLATOR: This will be a translator coment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" -msgstr[1] "" - -#: project/ignored/this_wont_normally_be_here.py:11 -msgid "FooBar" -msgid_plural "FooBars" -msgstr[0] "" -msgstr[1] "" - -""" % {'version': VERSION, - 'year': time.strftime('%Y'), - 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale='en')}, - open(pot_file, 'U').read()) - - def test_extraction_with_mapping_file(self): - self.cmd.copyright_holder = 'FooBar, Inc.' - self.cmd.msgid_bugs_address = 'bugs.address@email.tld' - self.cmd.mapping_file = 'mapping.cfg' - self.cmd.output_file = 'project/i18n/temp.pot' - self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:' - - self.cmd.finalize_options() - self.cmd.run() - - pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') - assert os.path.isfile(pot_file) - - self.assertEqual( -r"""# Translations template for TestProject. -# Copyright (C) %(year)s FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , %(year)s. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: %(date)s\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. TRANSLATOR: This will be a translator coment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" -msgstr[1] "" - -""" % {'version': VERSION, - 'year': time.strftime('%Y'), - 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale='en')}, - open(pot_file, 'U').read()) - - def test_extraction_with_mapping_dict(self): - self.dist.message_extractors = { - 'project': [ - ('**/ignored/**.*', 'ignore', None), - ('**.py', 'python', None), - ] - } - self.cmd.copyright_holder = 'FooBar, Inc.' - self.cmd.msgid_bugs_address = 'bugs.address@email.tld' - self.cmd.output_file = 'project/i18n/temp.pot' - self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:' - - self.cmd.finalize_options() - self.cmd.run() - - pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') - assert os.path.isfile(pot_file) - - self.assertEqual( -r"""# Translations template for TestProject. -# Copyright (C) %(year)s FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , %(year)s. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: %(date)s\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. TRANSLATOR: This will be a translator coment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" -msgstr[1] "" - -""" % {'version': VERSION, - 'year': time.strftime('%Y'), - 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale='en')}, - open(pot_file, 'U').read()) - - -class InitCatalogTestCase(unittest.TestCase): - - def setUp(self): - self.olddir = os.getcwd() - self.datadir = os.path.join(os.path.dirname(__file__), 'data') - os.chdir(self.datadir) - _global_log.threshold = 5 # shut up distutils logging - - self.dist = Distribution(dict( - name='TestProject', - version='0.1', - packages=['project'] - )) - self.cmd = frontend.init_catalog(self.dist) - self.cmd.initialize_options() - - def tearDown(self): - for dirname in ['en_US', 'ja_JP', 'lv_LV']: - locale_dir = os.path.join(self.datadir, 'project', 'i18n', dirname) - if os.path.isdir(locale_dir): - shutil.rmtree(locale_dir) - - os.chdir(self.olddir) - - def test_no_input_file(self): - self.cmd.locale = 'en_US' - self.cmd.output_file = 'dummy' - self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) - - def test_no_locale(self): - self.cmd.input_file = 'dummy' - self.cmd.output_file = 'dummy' - self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) - - def test_with_output_dir(self): - self.cmd.input_file = 'project/i18n/messages.pot' - self.cmd.locale = 'en_US' - self.cmd.output_dir = 'project/i18n' - - self.cmd.finalize_options() - self.cmd.run() - - po_file = os.path.join(self.datadir, 'project', 'i18n', 'en_US', - 'LC_MESSAGES', 'messages.po') - assert os.path.isfile(po_file) - - self.assertEqual( -r"""# English (United States) translations for TestProject. -# Copyright (C) 2007 FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , 2007. -# -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: 2007-04-01 15:30+0200\n" -"PO-Revision-Date: %(date)s\n" -"Last-Translator: FULL NAME \n" -"Language-Team: en_US \n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. This will be a translator coment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" -msgstr[1] "" - -""" % {'version': VERSION, - 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale='en')}, - open(po_file, 'U').read()) - - def test_keeps_catalog_non_fuzzy(self): - self.cmd.input_file = 'project/i18n/messages_non_fuzzy.pot' - self.cmd.locale = 'en_US' - self.cmd.output_dir = 'project/i18n' - - self.cmd.finalize_options() - self.cmd.run() - - po_file = os.path.join(self.datadir, 'project', 'i18n', 'en_US', - 'LC_MESSAGES', 'messages.po') - assert os.path.isfile(po_file) - - self.assertEqual( -r"""# English (United States) translations for TestProject. -# Copyright (C) 2007 FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , 2007. -# -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: 2007-04-01 15:30+0200\n" -"PO-Revision-Date: %(date)s\n" -"Last-Translator: FULL NAME \n" -"Language-Team: en_US \n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. This will be a translator coment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" -msgstr[1] "" - -""" % {'version': VERSION, - 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale='en')}, - open(po_file, 'U').read()) - - def test_correct_init_more_than_2_plurals(self): - self.cmd.input_file = 'project/i18n/messages.pot' - self.cmd.locale = 'lv_LV' - self.cmd.output_dir = 'project/i18n' - - self.cmd.finalize_options() - self.cmd.run() - - po_file = os.path.join(self.datadir, 'project', 'i18n', 'lv_LV', - 'LC_MESSAGES', 'messages.po') - assert os.path.isfile(po_file) - - self.assertEqual( -r"""# Latvian (Latvia) translations for TestProject. -# Copyright (C) 2007 FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , 2007. -# -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: 2007-04-01 15:30+0200\n" -"PO-Revision-Date: %(date)s\n" -"Last-Translator: FULL NAME \n" -"Language-Team: lv_LV \n" -"Plural-Forms: nplurals=3; plural=(n%%10==1 && n%%100!=11 ? 0 : n != 0 ? 1 :" -" 2)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. This will be a translator coment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" - -""" % {'version': VERSION, - 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale='en')}, - open(po_file, 'U').read()) - - def test_correct_init_singular_plural_forms(self): - self.cmd.input_file = 'project/i18n/messages.pot' - self.cmd.locale = 'ja_JP' - self.cmd.output_dir = 'project/i18n' - - self.cmd.finalize_options() - self.cmd.run() - - po_file = os.path.join(self.datadir, 'project', 'i18n', 'ja_JP', - 'LC_MESSAGES', 'messages.po') - assert os.path.isfile(po_file) - - self.assertEqual( -r"""# Japanese (Japan) translations for TestProject. -# Copyright (C) 2007 FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , 2007. -# -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: 2007-04-01 15:30+0200\n" -"PO-Revision-Date: %(date)s\n" -"Last-Translator: FULL NAME \n" -"Language-Team: ja_JP \n" -"Plural-Forms: nplurals=1; plural=0\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. This will be a translator coment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" - -""" % {'version': VERSION, - 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale='ja_JP')}, - open(po_file, 'U').read()) - - -class CommandLineInterfaceTestCase(unittest.TestCase): - - def setUp(self): - self.datadir = os.path.join(os.path.dirname(__file__), 'data') - self.orig_working_dir = os.getcwd() - self.orig_argv = sys.argv - self.orig_stdout = sys.stdout - self.orig_stderr = sys.stderr - sys.argv = ['pybabel'] - sys.stdout = StringIO() - sys.stderr = StringIO() - os.chdir(self.datadir) - - # Logging handlers will be reused if possible (#227). This breaks the - # implicit assumption that our newly created StringIO for sys.stderr - # contains the console output. Removing the old handler ensures that a - # new handler with our new StringIO instance will be used. - log = logging.getLogger('babel') - for handler in log.handlers: - log.removeHandler(handler) - self.cli = frontend.CommandLineInterface() - - def tearDown(self): - os.chdir(self.orig_working_dir) - sys.argv = self.orig_argv - sys.stdout = self.orig_stdout - sys.stderr = self.orig_stderr - for dirname in ['lv_LV', 'ja_JP']: - locale_dir = os.path.join(self.datadir, 'project', 'i18n', dirname) - if os.path.isdir(locale_dir): - shutil.rmtree(locale_dir) - - def test_usage(self): - try: - self.cli.run(sys.argv) - self.fail('Expected SystemExit') - except SystemExit, e: - stderr = sys.stderr.getvalue() - if isinstance(e.code, int): - self.assertEqual(2, e.code) - else: - # OptionParser in Python 2.3 does not set the exit code. - # Instead the 'code' contains the custom error message from the - # frontend - stderr = stderr + e.code + '\n' - self.assertEqual("""\ -usage: pybabel command [options] [args] - -pybabel: error: no valid command or option passed. try the -h/--help option for more information. -""", stderr.lower()) - - def _run_init_catalog(self): - i18n_dir = os.path.join(self.datadir, 'project', 'i18n') - pot_path = os.path.join(self.datadir, 'project', 'i18n', 'messages.pot') - init_argv = sys.argv + ['init', '--locale', 'en_US', '-d', i18n_dir, - '-i', pot_path] - self.cli.run(init_argv) - - def test_no_duplicated_output_for_multiple_runs(self): - self._run_init_catalog() - first_output = sys.stderr.getvalue() - self._run_init_catalog() - second_output = sys.stderr.getvalue()[len(first_output):] - - # in case the log message is not duplicated we should get the same - # output as before - self.assertEqual(first_output, second_output) - - def test_frontend_can_log_to_predefined_handler(self): - custom_stream = StringIO() - log = logging.getLogger('babel') - log.addHandler(logging.StreamHandler(custom_stream)) - - self._run_init_catalog() - self.assertNotEqual(id(sys.stderr), id(custom_stream)) - self.assertEqual('', sys.stderr.getvalue()) - assert len(custom_stream.getvalue()) > 0 - - def test_help(self): - try: - self.cli.run(sys.argv + ['--help']) - self.fail('Expected SystemExit') - except SystemExit, e: - self.assertEqual(0, e.code) - self.assertEqual("""\ -usage: pybabel command [options] [args] - -options: - --version show program's version number and exit - -h, --help show this help message and exit - --list-locales print all known locales and exit - -v, --verbose print as much as possible - -q, --quiet print as little as possible - -commands: - compile compile message catalogs to mo files - extract extract messages from source files and generate a pot file - init create new message catalogs from a pot file - update update existing message catalogs from a pot file -""", sys.stdout.getvalue().lower()) - - def test_extract_with_default_mapping(self): - pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') - self.cli.run(sys.argv + ['extract', - '--copyright-holder', 'FooBar, Inc.', - '--project', 'TestProject', '--version', '0.1', - '--msgid-bugs-address', 'bugs.address@email.tld', - '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:', - '-o', pot_file, 'project']) - assert os.path.isfile(pot_file) - self.assertEqual( -r"""# Translations template for TestProject. -# Copyright (C) %(year)s FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , %(year)s. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: %(date)s\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. TRANSLATOR: This will be a translator coment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" -msgstr[1] "" - -#: project/ignored/this_wont_normally_be_here.py:11 -msgid "FooBar" -msgid_plural "FooBars" -msgstr[0] "" -msgstr[1] "" - -""" % {'version': VERSION, - 'year': time.strftime('%Y'), - 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale='en')}, - open(pot_file, 'U').read()) - - def test_extract_with_mapping_file(self): - pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') - self.cli.run(sys.argv + ['extract', - '--copyright-holder', 'FooBar, Inc.', - '--project', 'TestProject', '--version', '0.1', - '--msgid-bugs-address', 'bugs.address@email.tld', - '--mapping', os.path.join(self.datadir, 'mapping.cfg'), - '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:', - '-o', pot_file, 'project']) - assert os.path.isfile(pot_file) - self.assertEqual( -r"""# Translations template for TestProject. -# Copyright (C) %(year)s FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , %(year)s. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: %(date)s\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. TRANSLATOR: This will be a translator coment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" -msgstr[1] "" - -""" % {'version': VERSION, - 'year': time.strftime('%Y'), - 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale='en')}, - open(pot_file, 'U').read()) - - def test_init_with_output_dir(self): - po_file = os.path.join(self.datadir, 'project', 'i18n', 'en_US', - 'LC_MESSAGES', 'messages.po') - self.cli.run(sys.argv + ['init', - '--locale', 'en_US', - '-d', os.path.join(self.datadir, 'project', 'i18n'), - '-i', os.path.join(self.datadir, 'project', 'i18n', 'messages.pot')]) - assert os.path.isfile(po_file) - self.assertEqual( -r"""# English (United States) translations for TestProject. -# Copyright (C) 2007 FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , 2007. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: 2007-04-01 15:30+0200\n" -"PO-Revision-Date: %(date)s\n" -"Last-Translator: FULL NAME \n" -"Language-Team: en_US \n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. This will be a translator coment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" -msgstr[1] "" - -""" % {'version': VERSION, - 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale='en')}, - open(po_file, 'U').read()) - - def test_init_singular_plural_forms(self): - po_file = os.path.join(self.datadir, 'project', 'i18n', 'ja_JP', - 'LC_MESSAGES', 'messages.po') - self.cli.run(sys.argv + ['init', - '--locale', 'ja_JP', - '-d', os.path.join(self.datadir, 'project', 'i18n'), - '-i', os.path.join(self.datadir, 'project', 'i18n', - 'messages.pot')]) - assert os.path.isfile(po_file) - self.assertEqual( -r"""# Japanese (Japan) translations for TestProject. -# Copyright (C) 2007 FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , 2007. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: 2007-04-01 15:30+0200\n" -"PO-Revision-Date: %(date)s\n" -"Last-Translator: FULL NAME \n" -"Language-Team: ja_JP \n" -"Plural-Forms: nplurals=1; plural=0\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. This will be a translator coment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" - -""" % {'version': VERSION, - 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale='en')}, - open(po_file, 'U').read()) - - def test_init_more_than_2_plural_forms(self): - po_file = os.path.join(self.datadir, 'project', 'i18n', 'lv_LV', - 'LC_MESSAGES', 'messages.po') - self.cli.run(sys.argv + ['init', - '--locale', 'lv_LV', - '-d', os.path.join(self.datadir, 'project', 'i18n'), - '-i', os.path.join(self.datadir, 'project', 'i18n', - 'messages.pot')]) - assert os.path.isfile(po_file) - self.assertEqual( -r"""# Latvian (Latvia) translations for TestProject. -# Copyright (C) 2007 FooBar, Inc. -# This file is distributed under the same license as the TestProject -# project. -# FIRST AUTHOR , 2007. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: TestProject 0.1\n" -"Report-Msgid-Bugs-To: bugs.address@email.tld\n" -"POT-Creation-Date: 2007-04-01 15:30+0200\n" -"PO-Revision-Date: %(date)s\n" -"Last-Translator: FULL NAME \n" -"Language-Team: lv_LV \n" -"Plural-Forms: nplurals=3; plural=(n%%10==1 && n%%100!=11 ? 0 : n != 0 ? 1 :" -" 2)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel %(version)s\n" - -#. This will be a translator coment, -#. that will include several lines -#: project/file1.py:8 -msgid "bar" -msgstr "" - -#: project/file2.py:9 -msgid "foobar" -msgid_plural "foobars" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" - -""" % {'version': VERSION, - 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', - tzinfo=LOCALTZ, locale='en')}, - open(po_file, 'U').read()) - - def test_compile_catalog(self): - po_file = os.path.join(self.datadir, 'project', 'i18n', 'de_DE', - 'LC_MESSAGES', 'messages.po') - mo_file = po_file.replace('.po', '.mo') - self.cli.run(sys.argv + ['compile', - '--locale', 'de_DE', - '-d', os.path.join(self.datadir, 'project', 'i18n')]) - assert not os.path.isfile(mo_file), 'Expected no file at %r' % mo_file - self.assertEqual("""\ -catalog %r is marked as fuzzy, skipping -""" % (po_file), sys.stderr.getvalue()) - - def test_compile_fuzzy_catalog(self): - po_file = os.path.join(self.datadir, 'project', 'i18n', 'de_DE', - 'LC_MESSAGES', 'messages.po') - mo_file = po_file.replace('.po', '.mo') - try: - self.cli.run(sys.argv + ['compile', - '--locale', 'de_DE', '--use-fuzzy', - '-d', os.path.join(self.datadir, 'project', 'i18n')]) - assert os.path.isfile(mo_file) - self.assertEqual("""\ -compiling catalog %r to %r -""" % (po_file, mo_file), sys.stderr.getvalue()) - finally: - if os.path.isfile(mo_file): - os.unlink(mo_file) - - def test_compile_catalog_with_more_than_2_plural_forms(self): - po_file = os.path.join(self.datadir, 'project', 'i18n', 'ru_RU', - 'LC_MESSAGES', 'messages.po') - mo_file = po_file.replace('.po', '.mo') - try: - self.cli.run(sys.argv + ['compile', - '--locale', 'ru_RU', '--use-fuzzy', - '-d', os.path.join(self.datadir, 'project', 'i18n')]) - assert os.path.isfile(mo_file) - self.assertEqual("""\ -compiling catalog %r to %r -""" % (po_file, mo_file), sys.stderr.getvalue()) - finally: - if os.path.isfile(mo_file): - os.unlink(mo_file) - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(frontend)) - suite.addTest(unittest.makeSuite(CompileCatalogTestCase)) - suite.addTest(unittest.makeSuite(ExtractMessagesTestCase)) - suite.addTest(unittest.makeSuite(InitCatalogTestCase)) - suite.addTest(unittest.makeSuite(CommandLineInterfaceTestCase)) - return suite - -if __name__ == '__main__': - unittest.main(defaultTest='suite') +# -*- 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/. + +from datetime import datetime +from distutils.dist import Distribution +from distutils.errors import DistutilsOptionError +from distutils.log import _global_log +import doctest +import logging +import os +import shutil +from StringIO import StringIO +import sys +import time +import unittest + +from babel import __version__ as VERSION +from babel.dates import format_datetime +from babel.messages import frontend +from babel.util import LOCALTZ + + +class CompileCatalogTestCase(unittest.TestCase): + + def setUp(self): + self.olddir = os.getcwd() + self.datadir = os.path.join(os.path.dirname(__file__), 'data') + os.chdir(self.datadir) + _global_log.threshold = 5 # shut up distutils logging + + self.dist = Distribution(dict( + name='TestProject', + version='0.1', + packages=['project'] + )) + self.cmd = frontend.compile_catalog(self.dist) + self.cmd.initialize_options() + + def tearDown(self): + os.chdir(self.olddir) + + def test_no_directory_or_output_file_specified(self): + self.cmd.locale = 'en_US' + self.cmd.input_file = 'dummy' + self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) + + def test_no_directory_or_input_file_specified(self): + self.cmd.locale = 'en_US' + self.cmd.output_file = 'dummy' + self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) + + +class ExtractMessagesTestCase(unittest.TestCase): + + def setUp(self): + self.olddir = os.getcwd() + self.datadir = os.path.join(os.path.dirname(__file__), 'data') + os.chdir(self.datadir) + _global_log.threshold = 5 # shut up distutils logging + + self.dist = Distribution(dict( + name='TestProject', + version='0.1', + packages=['project'] + )) + self.cmd = frontend.extract_messages(self.dist) + self.cmd.initialize_options() + + def tearDown(self): + pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') + if os.path.isfile(pot_file): + os.unlink(pot_file) + + os.chdir(self.olddir) + + def test_neither_default_nor_custom_keywords(self): + self.cmd.output_file = 'dummy' + self.cmd.no_default_keywords = True + self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) + + def test_no_output_file_specified(self): + self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) + + def test_both_sort_output_and_sort_by_file(self): + self.cmd.output_file = 'dummy' + self.cmd.sort_output = True + self.cmd.sort_by_file = True + self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) + + def test_extraction_with_default_mapping(self): + self.cmd.copyright_holder = 'FooBar, Inc.' + self.cmd.msgid_bugs_address = 'bugs.address@email.tld' + self.cmd.output_file = 'project/i18n/temp.pot' + self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:' + + self.cmd.finalize_options() + self.cmd.run() + + pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') + assert os.path.isfile(pot_file) + + self.assertEqual( +r"""# Translations template for TestProject. +# Copyright (C) %(year)s FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , %(year)s. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: %(date)s\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. TRANSLATOR: This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +#: project/ignored/this_wont_normally_be_here.py:11 +msgid "FooBar" +msgid_plural "FooBars" +msgstr[0] "" +msgstr[1] "" + +""" % {'version': VERSION, + 'year': time.strftime('%Y'), + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(pot_file, 'U').read()) + + def test_extraction_with_mapping_file(self): + self.cmd.copyright_holder = 'FooBar, Inc.' + self.cmd.msgid_bugs_address = 'bugs.address@email.tld' + self.cmd.mapping_file = 'mapping.cfg' + self.cmd.output_file = 'project/i18n/temp.pot' + self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:' + + self.cmd.finalize_options() + self.cmd.run() + + pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') + assert os.path.isfile(pot_file) + + self.assertEqual( +r"""# Translations template for TestProject. +# Copyright (C) %(year)s FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , %(year)s. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: %(date)s\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. TRANSLATOR: This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +""" % {'version': VERSION, + 'year': time.strftime('%Y'), + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(pot_file, 'U').read()) + + def test_extraction_with_mapping_dict(self): + self.dist.message_extractors = { + 'project': [ + ('**/ignored/**.*', 'ignore', None), + ('**.py', 'python', None), + ] + } + self.cmd.copyright_holder = 'FooBar, Inc.' + self.cmd.msgid_bugs_address = 'bugs.address@email.tld' + self.cmd.output_file = 'project/i18n/temp.pot' + self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:' + + self.cmd.finalize_options() + self.cmd.run() + + pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') + assert os.path.isfile(pot_file) + + self.assertEqual( +r"""# Translations template for TestProject. +# Copyright (C) %(year)s FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , %(year)s. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: %(date)s\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. TRANSLATOR: This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +""" % {'version': VERSION, + 'year': time.strftime('%Y'), + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(pot_file, 'U').read()) + + +class InitCatalogTestCase(unittest.TestCase): + + def setUp(self): + self.olddir = os.getcwd() + self.datadir = os.path.join(os.path.dirname(__file__), 'data') + os.chdir(self.datadir) + _global_log.threshold = 5 # shut up distutils logging + + self.dist = Distribution(dict( + name='TestProject', + version='0.1', + packages=['project'] + )) + self.cmd = frontend.init_catalog(self.dist) + self.cmd.initialize_options() + + def tearDown(self): + for dirname in ['en_US', 'ja_JP', 'lv_LV']: + locale_dir = os.path.join(self.datadir, 'project', 'i18n', dirname) + if os.path.isdir(locale_dir): + shutil.rmtree(locale_dir) + + os.chdir(self.olddir) + + def test_no_input_file(self): + self.cmd.locale = 'en_US' + self.cmd.output_file = 'dummy' + self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) + + def test_no_locale(self): + self.cmd.input_file = 'dummy' + self.cmd.output_file = 'dummy' + self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) + + def test_with_output_dir(self): + self.cmd.input_file = 'project/i18n/messages.pot' + self.cmd.locale = 'en_US' + self.cmd.output_dir = 'project/i18n' + + self.cmd.finalize_options() + self.cmd.run() + + po_file = os.path.join(self.datadir, 'project', 'i18n', 'en_US', + 'LC_MESSAGES', 'messages.po') + assert os.path.isfile(po_file) + + self.assertEqual( +r"""# English (United States) translations for TestProject. +# Copyright (C) 2007 FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , 2007. +# +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: 2007-04-01 15:30+0200\n" +"PO-Revision-Date: %(date)s\n" +"Last-Translator: FULL NAME \n" +"Language-Team: en_US \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +""" % {'version': VERSION, + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(po_file, 'U').read()) + + def test_keeps_catalog_non_fuzzy(self): + self.cmd.input_file = 'project/i18n/messages_non_fuzzy.pot' + self.cmd.locale = 'en_US' + self.cmd.output_dir = 'project/i18n' + + self.cmd.finalize_options() + self.cmd.run() + + po_file = os.path.join(self.datadir, 'project', 'i18n', 'en_US', + 'LC_MESSAGES', 'messages.po') + assert os.path.isfile(po_file) + + self.assertEqual( +r"""# English (United States) translations for TestProject. +# Copyright (C) 2007 FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , 2007. +# +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: 2007-04-01 15:30+0200\n" +"PO-Revision-Date: %(date)s\n" +"Last-Translator: FULL NAME \n" +"Language-Team: en_US \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +""" % {'version': VERSION, + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(po_file, 'U').read()) + + def test_correct_init_more_than_2_plurals(self): + self.cmd.input_file = 'project/i18n/messages.pot' + self.cmd.locale = 'lv_LV' + self.cmd.output_dir = 'project/i18n' + + self.cmd.finalize_options() + self.cmd.run() + + po_file = os.path.join(self.datadir, 'project', 'i18n', 'lv_LV', + 'LC_MESSAGES', 'messages.po') + assert os.path.isfile(po_file) + + self.assertEqual( +r"""# Latvian (Latvia) translations for TestProject. +# Copyright (C) 2007 FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , 2007. +# +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: 2007-04-01 15:30+0200\n" +"PO-Revision-Date: %(date)s\n" +"Last-Translator: FULL NAME \n" +"Language-Team: lv_LV \n" +"Plural-Forms: nplurals=3; plural=(n%%10==1 && n%%100!=11 ? 0 : n != 0 ? 1 :" +" 2)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +""" % {'version': VERSION, + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(po_file, 'U').read()) + + def test_correct_init_singular_plural_forms(self): + self.cmd.input_file = 'project/i18n/messages.pot' + self.cmd.locale = 'ja_JP' + self.cmd.output_dir = 'project/i18n' + + self.cmd.finalize_options() + self.cmd.run() + + po_file = os.path.join(self.datadir, 'project', 'i18n', 'ja_JP', + 'LC_MESSAGES', 'messages.po') + assert os.path.isfile(po_file) + + self.assertEqual( +r"""# Japanese (Japan) translations for TestProject. +# Copyright (C) 2007 FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , 2007. +# +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: 2007-04-01 15:30+0200\n" +"PO-Revision-Date: %(date)s\n" +"Last-Translator: FULL NAME \n" +"Language-Team: ja_JP \n" +"Plural-Forms: nplurals=1; plural=0\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" + +""" % {'version': VERSION, + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='ja_JP')}, + open(po_file, 'U').read()) + + +class CommandLineInterfaceTestCase(unittest.TestCase): + + def setUp(self): + self.datadir = os.path.join(os.path.dirname(__file__), 'data') + self.orig_working_dir = os.getcwd() + self.orig_argv = sys.argv + self.orig_stdout = sys.stdout + self.orig_stderr = sys.stderr + sys.argv = ['pybabel'] + sys.stdout = StringIO() + sys.stderr = StringIO() + os.chdir(self.datadir) + + # Logging handlers will be reused if possible (#227). This breaks the + # implicit assumption that our newly created StringIO for sys.stderr + # contains the console output. Removing the old handler ensures that a + # new handler with our new StringIO instance will be used. + log = logging.getLogger('babel') + for handler in log.handlers: + log.removeHandler(handler) + self.cli = frontend.CommandLineInterface() + + def tearDown(self): + os.chdir(self.orig_working_dir) + sys.argv = self.orig_argv + sys.stdout = self.orig_stdout + sys.stderr = self.orig_stderr + for dirname in ['lv_LV', 'ja_JP']: + locale_dir = os.path.join(self.datadir, 'project', 'i18n', dirname) + if os.path.isdir(locale_dir): + shutil.rmtree(locale_dir) + + def test_usage(self): + try: + self.cli.run(sys.argv) + self.fail('Expected SystemExit') + except SystemExit, e: + stderr = sys.stderr.getvalue() + if isinstance(e.code, int): + self.assertEqual(2, e.code) + else: + # OptionParser in Python 2.3 does not set the exit code. + # Instead the 'code' contains the custom error message from the + # frontend + stderr = stderr + e.code + '\n' + self.assertEqual("""\ +usage: pybabel command [options] [args] + +pybabel: error: no valid command or option passed. try the -h/--help option for more information. +""", stderr.lower()) + + def _run_init_catalog(self): + i18n_dir = os.path.join(self.datadir, 'project', 'i18n') + pot_path = os.path.join(self.datadir, 'project', 'i18n', 'messages.pot') + init_argv = sys.argv + ['init', '--locale', 'en_US', '-d', i18n_dir, + '-i', pot_path] + self.cli.run(init_argv) + + def test_no_duplicated_output_for_multiple_runs(self): + self._run_init_catalog() + first_output = sys.stderr.getvalue() + self._run_init_catalog() + second_output = sys.stderr.getvalue()[len(first_output):] + + # in case the log message is not duplicated we should get the same + # output as before + self.assertEqual(first_output, second_output) + + def test_frontend_can_log_to_predefined_handler(self): + custom_stream = StringIO() + log = logging.getLogger('babel') + log.addHandler(logging.StreamHandler(custom_stream)) + + self._run_init_catalog() + self.assertNotEqual(id(sys.stderr), id(custom_stream)) + self.assertEqual('', sys.stderr.getvalue()) + assert len(custom_stream.getvalue()) > 0 + + def test_help(self): + try: + self.cli.run(sys.argv + ['--help']) + self.fail('Expected SystemExit') + except SystemExit, e: + self.assertEqual(0, e.code) + self.assertEqual("""\ +usage: pybabel command [options] [args] + +options: + --version show program's version number and exit + -h, --help show this help message and exit + --list-locales print all known locales and exit + -v, --verbose print as much as possible + -q, --quiet print as little as possible + +commands: + compile compile message catalogs to mo files + extract extract messages from source files and generate a pot file + init create new message catalogs from a pot file + update update existing message catalogs from a pot file +""", sys.stdout.getvalue().lower()) + + def test_extract_with_default_mapping(self): + pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') + self.cli.run(sys.argv + ['extract', + '--copyright-holder', 'FooBar, Inc.', + '--project', 'TestProject', '--version', '0.1', + '--msgid-bugs-address', 'bugs.address@email.tld', + '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:', + '-o', pot_file, 'project']) + assert os.path.isfile(pot_file) + self.assertEqual( +r"""# Translations template for TestProject. +# Copyright (C) %(year)s FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , %(year)s. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: %(date)s\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. TRANSLATOR: This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +#: project/ignored/this_wont_normally_be_here.py:11 +msgid "FooBar" +msgid_plural "FooBars" +msgstr[0] "" +msgstr[1] "" + +""" % {'version': VERSION, + 'year': time.strftime('%Y'), + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(pot_file, 'U').read()) + + def test_extract_with_mapping_file(self): + pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') + self.cli.run(sys.argv + ['extract', + '--copyright-holder', 'FooBar, Inc.', + '--project', 'TestProject', '--version', '0.1', + '--msgid-bugs-address', 'bugs.address@email.tld', + '--mapping', os.path.join(self.datadir, 'mapping.cfg'), + '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:', + '-o', pot_file, 'project']) + assert os.path.isfile(pot_file) + self.assertEqual( +r"""# Translations template for TestProject. +# Copyright (C) %(year)s FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , %(year)s. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: %(date)s\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. TRANSLATOR: This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +""" % {'version': VERSION, + 'year': time.strftime('%Y'), + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(pot_file, 'U').read()) + + def test_init_with_output_dir(self): + po_file = os.path.join(self.datadir, 'project', 'i18n', 'en_US', + 'LC_MESSAGES', 'messages.po') + self.cli.run(sys.argv + ['init', + '--locale', 'en_US', + '-d', os.path.join(self.datadir, 'project', 'i18n'), + '-i', os.path.join(self.datadir, 'project', 'i18n', 'messages.pot')]) + assert os.path.isfile(po_file) + self.assertEqual( +r"""# English (United States) translations for TestProject. +# Copyright (C) 2007 FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , 2007. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: 2007-04-01 15:30+0200\n" +"PO-Revision-Date: %(date)s\n" +"Last-Translator: FULL NAME \n" +"Language-Team: en_US \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +""" % {'version': VERSION, + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(po_file, 'U').read()) + + def test_init_singular_plural_forms(self): + po_file = os.path.join(self.datadir, 'project', 'i18n', 'ja_JP', + 'LC_MESSAGES', 'messages.po') + self.cli.run(sys.argv + ['init', + '--locale', 'ja_JP', + '-d', os.path.join(self.datadir, 'project', 'i18n'), + '-i', os.path.join(self.datadir, 'project', 'i18n', + 'messages.pot')]) + assert os.path.isfile(po_file) + self.assertEqual( +r"""# Japanese (Japan) translations for TestProject. +# Copyright (C) 2007 FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , 2007. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: 2007-04-01 15:30+0200\n" +"PO-Revision-Date: %(date)s\n" +"Last-Translator: FULL NAME \n" +"Language-Team: ja_JP \n" +"Plural-Forms: nplurals=1; plural=0\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" + +""" % {'version': VERSION, + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(po_file, 'U').read()) + + def test_init_more_than_2_plural_forms(self): + po_file = os.path.join(self.datadir, 'project', 'i18n', 'lv_LV', + 'LC_MESSAGES', 'messages.po') + self.cli.run(sys.argv + ['init', + '--locale', 'lv_LV', + '-d', os.path.join(self.datadir, 'project', 'i18n'), + '-i', os.path.join(self.datadir, 'project', 'i18n', + 'messages.pot')]) + assert os.path.isfile(po_file) + self.assertEqual( +r"""# Latvian (Latvia) translations for TestProject. +# Copyright (C) 2007 FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR , 2007. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: 2007-04-01 15:30+0200\n" +"PO-Revision-Date: %(date)s\n" +"Last-Translator: FULL NAME \n" +"Language-Team: lv_LV \n" +"Plural-Forms: nplurals=3; plural=(n%%10==1 && n%%100!=11 ? 0 : n != 0 ? 1 :" +" 2)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +""" % {'version': VERSION, + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(po_file, 'U').read()) + + def test_compile_catalog(self): + po_file = os.path.join(self.datadir, 'project', 'i18n', 'de_DE', + 'LC_MESSAGES', 'messages.po') + mo_file = po_file.replace('.po', '.mo') + self.cli.run(sys.argv + ['compile', + '--locale', 'de_DE', + '-d', os.path.join(self.datadir, 'project', 'i18n')]) + assert not os.path.isfile(mo_file), 'Expected no file at %r' % mo_file + self.assertEqual("""\ +catalog %r is marked as fuzzy, skipping +""" % (po_file), sys.stderr.getvalue()) + + def test_compile_fuzzy_catalog(self): + po_file = os.path.join(self.datadir, 'project', 'i18n', 'de_DE', + 'LC_MESSAGES', 'messages.po') + mo_file = po_file.replace('.po', '.mo') + try: + self.cli.run(sys.argv + ['compile', + '--locale', 'de_DE', '--use-fuzzy', + '-d', os.path.join(self.datadir, 'project', 'i18n')]) + assert os.path.isfile(mo_file) + self.assertEqual("""\ +compiling catalog %r to %r +""" % (po_file, mo_file), sys.stderr.getvalue()) + finally: + if os.path.isfile(mo_file): + os.unlink(mo_file) + + def test_compile_catalog_with_more_than_2_plural_forms(self): + po_file = os.path.join(self.datadir, 'project', 'i18n', 'ru_RU', + 'LC_MESSAGES', 'messages.po') + mo_file = po_file.replace('.po', '.mo') + try: + self.cli.run(sys.argv + ['compile', + '--locale', 'ru_RU', '--use-fuzzy', + '-d', os.path.join(self.datadir, 'project', 'i18n')]) + assert os.path.isfile(mo_file) + self.assertEqual("""\ +compiling catalog %r to %r +""" % (po_file, mo_file), sys.stderr.getvalue()) + finally: + if os.path.isfile(mo_file): + os.unlink(mo_file) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(frontend)) + suite.addTest(unittest.makeSuite(CompileCatalogTestCase)) + suite.addTest(unittest.makeSuite(ExtractMessagesTestCase)) + suite.addTest(unittest.makeSuite(InitCatalogTestCase)) + suite.addTest(unittest.makeSuite(CommandLineInterfaceTestCase)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/justice/external/babel/messages/tests/mofile.py b/justice/external/babel/messages/tests/mofile.py index 3199c75..9a08c83 100644 --- a/justice/external/babel/messages/tests/mofile.py +++ b/justice/external/babel/messages/tests/mofile.py @@ -1,64 +1,64 @@ -# -*- 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 -import gettext -import unittest -from StringIO import StringIO - -from babel.messages import mofile, Catalog - -class WriteMoTestCase(unittest.TestCase): - - def test_sorting(self): - # Ensure the header is sorted to the first entry so that its charset - # can be applied to all subsequent messages by GNUTranslations - # (ensuring all messages are safely converted to unicode) - catalog = Catalog(locale='en_US') - catalog.add(u'', '''\ -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n''') - catalog.add(u'foo', 'Voh') - catalog.add((u'There is', u'There are'), (u'Es gibt', u'Es gibt')) - catalog.add(u'Fizz', '') - catalog.add(('Fuzz', 'Fuzzes'), ('', '')) - buf = StringIO() - mofile.write_mo(buf, catalog) - buf.seek(0) - translations = gettext.GNUTranslations(fp=buf) - self.assertEqual(u'Voh', translations.ugettext('foo')) - assert isinstance(translations.ugettext('foo'), unicode) - self.assertEqual(u'Es gibt', translations.ungettext('There is', 'There are', 1)) - assert isinstance(translations.ungettext('There is', 'There are', 1), unicode) - self.assertEqual(u'Fizz', translations.ugettext('Fizz')) - assert isinstance(translations.ugettext('Fizz'), unicode) - self.assertEqual(u'Fuzz', translations.ugettext('Fuzz')) - assert isinstance(translations.ugettext('Fuzz'), unicode) - self.assertEqual(u'Fuzzes', translations.ugettext('Fuzzes')) - assert isinstance(translations.ugettext('Fuzzes'), unicode) - - def test_more_plural_forms(self): - catalog2 = Catalog(locale='ru_RU') - catalog2.add(('Fuzz', 'Fuzzes'), ('', '', '')) - buf = StringIO() - mofile.write_mo(buf, catalog2) - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(mofile)) - suite.addTest(unittest.makeSuite(WriteMoTestCase)) - return suite - -if __name__ == '__main__': - unittest.main(defaultTest='suite') +# -*- 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 +import gettext +import unittest +from StringIO import StringIO + +from babel.messages import mofile, Catalog + +class WriteMoTestCase(unittest.TestCase): + + def test_sorting(self): + # Ensure the header is sorted to the first entry so that its charset + # can be applied to all subsequent messages by GNUTranslations + # (ensuring all messages are safely converted to unicode) + catalog = Catalog(locale='en_US') + catalog.add(u'', '''\ +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n''') + catalog.add(u'foo', 'Voh') + catalog.add((u'There is', u'There are'), (u'Es gibt', u'Es gibt')) + catalog.add(u'Fizz', '') + catalog.add(('Fuzz', 'Fuzzes'), ('', '')) + buf = StringIO() + mofile.write_mo(buf, catalog) + buf.seek(0) + translations = gettext.GNUTranslations(fp=buf) + self.assertEqual(u'Voh', translations.ugettext('foo')) + assert isinstance(translations.ugettext('foo'), unicode) + self.assertEqual(u'Es gibt', translations.ungettext('There is', 'There are', 1)) + assert isinstance(translations.ungettext('There is', 'There are', 1), unicode) + self.assertEqual(u'Fizz', translations.ugettext('Fizz')) + assert isinstance(translations.ugettext('Fizz'), unicode) + self.assertEqual(u'Fuzz', translations.ugettext('Fuzz')) + assert isinstance(translations.ugettext('Fuzz'), unicode) + self.assertEqual(u'Fuzzes', translations.ugettext('Fuzzes')) + assert isinstance(translations.ugettext('Fuzzes'), unicode) + + def test_more_plural_forms(self): + catalog2 = Catalog(locale='ru_RU') + catalog2.add(('Fuzz', 'Fuzzes'), ('', '', '')) + buf = StringIO() + mofile.write_mo(buf, catalog2) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(mofile)) + suite.addTest(unittest.makeSuite(WriteMoTestCase)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/justice/external/babel/messages/tests/plurals.py b/justice/external/babel/messages/tests/plurals.py index 211840a..f573ab1 100644 --- a/justice/external/babel/messages/tests/plurals.py +++ b/justice/external/babel/messages/tests/plurals.py @@ -1,25 +1,25 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2008 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.messages import plurals - -def suite(): - suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(plurals)) - return suite - -if __name__ == '__main__': - unittest.main(defaultTest='suite') +# -*- coding: utf-8 -*- +# +# Copyright (C) 2008 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.messages import plurals + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(plurals)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/justice/external/babel/messages/tests/pofile.py b/justice/external/babel/messages/tests/pofile.py index 2e1470d..06538fd 100644 --- a/justice/external/babel/messages/tests/pofile.py +++ b/justice/external/babel/messages/tests/pofile.py @@ -1,463 +1,463 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007-2010 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/. - -from datetime import datetime -import doctest -from StringIO import StringIO -import unittest -import sys - -from babel.messages.catalog import Catalog, Message -from babel.messages import pofile -from babel.util import FixedOffsetTimezone, LOCALTZ - - -class ReadPoTestCase(unittest.TestCase): - - def test_preserve_locale(self): - buf = StringIO(r'''msgid "foo" -msgstr "Voh"''') - catalog = pofile.read_po(buf, locale='en_US') - self.assertEqual('en_US', catalog.locale) - - def test_preserve_domain(self): - buf = StringIO(r'''msgid "foo" -msgstr "Voh"''') - catalog = pofile.read_po(buf, domain='mydomain') - self.assertEqual('mydomain', catalog.domain) - - def test_read_multiline(self): - buf = StringIO(r'''msgid "" -"Here's some text that\n" -"includesareallylongwordthatmightbutshouldnt" -" throw us into an infinite " -"loop\n" -msgstr ""''') - catalog = pofile.read_po(buf) - self.assertEqual(1, len(catalog)) - message = list(catalog)[1] - self.assertEqual("Here's some text that\nincludesareallylongwordthat" - "mightbutshouldnt throw us into an infinite loop\n", - message.id) - - def test_fuzzy_header(self): - buf = StringIO(r'''\ -# Translations template for AReallyReallyLongNameForAProject. -# Copyright (C) 2007 ORGANIZATION -# This file is distributed under the same license as the -# AReallyReallyLongNameForAProject project. -# FIRST AUTHOR , 2007. -# -#, fuzzy -''') - catalog = pofile.read_po(buf) - self.assertEqual(1, len(list(catalog))) - self.assertEqual(True, list(catalog)[0].fuzzy) - - def test_not_fuzzy_header(self): - buf = StringIO(r'''\ -# Translations template for AReallyReallyLongNameForAProject. -# Copyright (C) 2007 ORGANIZATION -# This file is distributed under the same license as the -# AReallyReallyLongNameForAProject project. -# FIRST AUTHOR , 2007. -# -''') - catalog = pofile.read_po(buf) - self.assertEqual(1, len(list(catalog))) - self.assertEqual(False, list(catalog)[0].fuzzy) - - def test_header_entry(self): - buf = StringIO(r'''\ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) 2007 THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , 2007. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: 3.15\n" -"Report-Msgid-Bugs-To: Fliegender Zirkus \n" -"POT-Creation-Date: 2007-09-27 11:19+0700\n" -"PO-Revision-Date: 2007-09-27 21:42-0700\n" -"Last-Translator: John \n" -"Language-Team: German Lang \n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=iso-8859-2\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.0dev-r313\n" -''') - catalog = pofile.read_po(buf) - self.assertEqual(1, len(list(catalog))) - self.assertEqual(u'3.15', catalog.version) - self.assertEqual(u'Fliegender Zirkus ', - catalog.msgid_bugs_address) - self.assertEqual(datetime(2007, 9, 27, 11, 19, - tzinfo=FixedOffsetTimezone(7 * 60)), - catalog.creation_date) - self.assertEqual(u'John ', catalog.last_translator) - self.assertEqual(u'German Lang ', catalog.language_team) - self.assertEqual(u'iso-8859-2', catalog.charset) - self.assertEqual(True, list(catalog)[0].fuzzy) - - def test_obsolete_message(self): - buf = StringIO(r'''# This is an obsolete message -#~ msgid "foo" -#~ msgstr "Voh" - -# This message is not obsolete -#: main.py:1 -msgid "bar" -msgstr "Bahr" -''') - catalog = pofile.read_po(buf) - self.assertEqual(1, len(catalog)) - self.assertEqual(1, len(catalog.obsolete)) - message = catalog.obsolete[u'foo'] - self.assertEqual(u'foo', message.id) - self.assertEqual(u'Voh', message.string) - self.assertEqual(['This is an obsolete message'], message.user_comments) - - def test_obsolete_message_ignored(self): - buf = StringIO(r'''# This is an obsolete message -#~ msgid "foo" -#~ msgstr "Voh" - -# This message is not obsolete -#: main.py:1 -msgid "bar" -msgstr "Bahr" -''') - catalog = pofile.read_po(buf, ignore_obsolete=True) - self.assertEqual(1, len(catalog)) - self.assertEqual(0, len(catalog.obsolete)) - - def test_single_plural_form(self): - buf = StringIO(r'''msgid "foo" -msgid_plural "foos" -msgstr[0] "Voh"''') - catalog = pofile.read_po(buf, locale='ja_JP') - self.assertEqual(1, len(catalog)) - self.assertEqual(1, catalog.num_plurals) - message = catalog['foo'] - self.assertEqual(1, len(message.string)) - - def test_singular_plural_form(self): - buf = StringIO(r'''msgid "foo" -msgid_plural "foos" -msgstr[0] "Voh" -msgstr[1] "Vohs"''') - catalog = pofile.read_po(buf, locale='nl_NL') - self.assertEqual(1, len(catalog)) - self.assertEqual(2, catalog.num_plurals) - message = catalog['foo'] - self.assertEqual(2, len(message.string)) - - def test_more_than_two_plural_forms(self): - buf = StringIO(r'''msgid "foo" -msgid_plural "foos" -msgstr[0] "Voh" -msgstr[1] "Vohs" -msgstr[2] "Vohss"''') - catalog = pofile.read_po(buf, locale='lv_LV') - self.assertEqual(1, len(catalog)) - self.assertEqual(3, catalog.num_plurals) - message = catalog['foo'] - self.assertEqual(3, len(message.string)) - self.assertEqual(u'Vohss', message.string[2]) - - def test_plural_with_square_brackets(self): - buf = StringIO(r'''msgid "foo" -msgid_plural "foos" -msgstr[0] "Voh [text]" -msgstr[1] "Vohs [text]"''') - catalog = pofile.read_po(buf, locale='nb_NO') - self.assertEqual(1, len(catalog)) - self.assertEqual(2, catalog.num_plurals) - message = catalog['foo'] - self.assertEqual(2, len(message.string)) - - -class WritePoTestCase(unittest.TestCase): - - def test_join_locations(self): - catalog = Catalog() - catalog.add(u'foo', locations=[('main.py', 1)]) - catalog.add(u'foo', locations=[('utils.py', 3)]) - buf = StringIO() - pofile.write_po(buf, catalog, omit_header=True) - self.assertEqual('''#: main.py:1 utils.py:3 -msgid "foo" -msgstr ""''', buf.getvalue().strip()) - - def test_duplicate_comments(self): - catalog = Catalog() - catalog.add(u'foo', auto_comments=['A comment']) - catalog.add(u'foo', auto_comments=['A comment']) - buf = StringIO() - pofile.write_po(buf, catalog, omit_header=True) - self.assertEqual('''#. A comment -msgid "foo" -msgstr ""''', buf.getvalue().strip()) - - def test_wrap_long_lines(self): - text = """Here's some text where -white space and line breaks matter, and should - -not be removed - -""" - catalog = Catalog() - catalog.add(text, locations=[('main.py', 1)]) - buf = StringIO() - pofile.write_po(buf, catalog, no_location=True, omit_header=True, - width=42) - self.assertEqual(r'''msgid "" -"Here's some text where \n" -"white space and line breaks matter, and" -" should\n" -"\n" -"not be removed\n" -"\n" -msgstr ""''', buf.getvalue().strip()) - - def test_wrap_long_lines_with_long_word(self): - text = """Here's some text that -includesareallylongwordthatmightbutshouldnt throw us into an infinite loop -""" - catalog = Catalog() - catalog.add(text, locations=[('main.py', 1)]) - buf = StringIO() - pofile.write_po(buf, catalog, no_location=True, omit_header=True, - width=32) - self.assertEqual(r'''msgid "" -"Here's some text that\n" -"includesareallylongwordthatmightbutshouldnt" -" throw us into an infinite " -"loop\n" -msgstr ""''', buf.getvalue().strip()) - - def test_wrap_long_lines_in_header(self): - """ - Verify that long lines in the header comment are wrapped correctly. - """ - catalog = Catalog(project='AReallyReallyLongNameForAProject', - revision_date=datetime(2007, 4, 1)) - buf = StringIO() - pofile.write_po(buf, catalog) - self.assertEqual('''\ -# Translations template for AReallyReallyLongNameForAProject. -# Copyright (C) 2007 ORGANIZATION -# This file is distributed under the same license as the -# AReallyReallyLongNameForAProject project. -# FIRST AUTHOR , 2007. -# -#, fuzzy''', '\n'.join(buf.getvalue().splitlines()[:7])) - - def test_wrap_locations_with_hyphens(self): - catalog = Catalog() - catalog.add(u'foo', locations=[ - ('doupy/templates/base/navmenu.inc.html.py', 60) - ]) - catalog.add(u'foo', locations=[ - ('doupy/templates/job-offers/helpers.html', 22) - ]) - buf = StringIO() - pofile.write_po(buf, catalog, omit_header=True) - self.assertEqual('''#: doupy/templates/base/navmenu.inc.html.py:60 -#: doupy/templates/job-offers/helpers.html:22 -msgid "foo" -msgstr ""''', buf.getvalue().strip()) - - def test_no_wrap_and_width_behaviour_on_comments(self): - catalog = Catalog() - catalog.add("Pretty dam long message id, which must really be big " - "to test this wrap behaviour, if not it won't work.", - locations=[("fake.py", n) for n in range(1, 30)]) - buf = StringIO() - pofile.write_po(buf, catalog, width=None, omit_header=True) - self.assertEqual("""\ -#: fake.py:1 fake.py:2 fake.py:3 fake.py:4 fake.py:5 fake.py:6 fake.py:7 -#: fake.py:8 fake.py:9 fake.py:10 fake.py:11 fake.py:12 fake.py:13 fake.py:14 -#: fake.py:15 fake.py:16 fake.py:17 fake.py:18 fake.py:19 fake.py:20 fake.py:21 -#: fake.py:22 fake.py:23 fake.py:24 fake.py:25 fake.py:26 fake.py:27 fake.py:28 -#: fake.py:29 -msgid "pretty dam long message id, which must really be big to test this wrap behaviour, if not it won't work." -msgstr "" - -""", buf.getvalue().lower()) - buf = StringIO() - pofile.write_po(buf, catalog, width=100, omit_header=True) - self.assertEqual("""\ -#: fake.py:1 fake.py:2 fake.py:3 fake.py:4 fake.py:5 fake.py:6 fake.py:7 fake.py:8 fake.py:9 fake.py:10 -#: fake.py:11 fake.py:12 fake.py:13 fake.py:14 fake.py:15 fake.py:16 fake.py:17 fake.py:18 fake.py:19 -#: fake.py:20 fake.py:21 fake.py:22 fake.py:23 fake.py:24 fake.py:25 fake.py:26 fake.py:27 fake.py:28 -#: fake.py:29 -msgid "" -"pretty dam long message id, which must really be big to test this wrap behaviour, if not it won't" -" work." -msgstr "" - -""", buf.getvalue().lower()) - - def test_pot_with_translator_comments(self): - catalog = Catalog() - catalog.add(u'foo', locations=[('main.py', 1)], - auto_comments=['Comment About `foo`']) - catalog.add(u'bar', locations=[('utils.py', 3)], - user_comments=['Comment About `bar` with', - 'multiple lines.']) - buf = StringIO() - pofile.write_po(buf, catalog, omit_header=True) - self.assertEqual('''#. Comment About `foo` -#: main.py:1 -msgid "foo" -msgstr "" - -# Comment About `bar` with -# multiple lines. -#: utils.py:3 -msgid "bar" -msgstr ""''', buf.getvalue().strip()) - - def test_po_with_obsolete_message(self): - catalog = Catalog() - catalog.add(u'foo', u'Voh', locations=[('main.py', 1)]) - catalog.obsolete['bar'] = Message(u'bar', u'Bahr', - locations=[('utils.py', 3)], - user_comments=['User comment']) - buf = StringIO() - pofile.write_po(buf, catalog, omit_header=True) - self.assertEqual('''#: main.py:1 -msgid "foo" -msgstr "Voh" - -# User comment -#~ msgid "bar" -#~ msgstr "Bahr"''', buf.getvalue().strip()) - - def test_po_with_multiline_obsolete_message(self): - catalog = Catalog() - catalog.add(u'foo', u'Voh', locations=[('main.py', 1)]) - msgid = r"""Here's a message that covers -multiple lines, and should still be handled -correctly. -""" - msgstr = r"""Here's a message that covers -multiple lines, and should still be handled -correctly. -""" - catalog.obsolete[msgid] = Message(msgid, msgstr, - locations=[('utils.py', 3)]) - buf = StringIO() - pofile.write_po(buf, catalog, omit_header=True) - self.assertEqual(r'''#: main.py:1 -msgid "foo" -msgstr "Voh" - -#~ msgid "" -#~ "Here's a message that covers\n" -#~ "multiple lines, and should still be handled\n" -#~ "correctly.\n" -#~ msgstr "" -#~ "Here's a message that covers\n" -#~ "multiple lines, and should still be handled\n" -#~ "correctly.\n"''', buf.getvalue().strip()) - - def test_po_with_obsolete_message_ignored(self): - catalog = Catalog() - catalog.add(u'foo', u'Voh', locations=[('main.py', 1)]) - catalog.obsolete['bar'] = Message(u'bar', u'Bahr', - locations=[('utils.py', 3)], - user_comments=['User comment']) - buf = StringIO() - pofile.write_po(buf, catalog, omit_header=True, ignore_obsolete=True) - self.assertEqual('''#: main.py:1 -msgid "foo" -msgstr "Voh"''', buf.getvalue().strip()) - - def test_po_with_previous_msgid(self): - catalog = Catalog() - catalog.add(u'foo', u'Voh', locations=[('main.py', 1)], - previous_id=u'fo') - buf = StringIO() - pofile.write_po(buf, catalog, omit_header=True, include_previous=True) - self.assertEqual('''#: main.py:1 -#| msgid "fo" -msgid "foo" -msgstr "Voh"''', buf.getvalue().strip()) - - def test_po_with_previous_msgid_plural(self): - catalog = Catalog() - catalog.add((u'foo', u'foos'), (u'Voh', u'Voeh'), - locations=[('main.py', 1)], previous_id=(u'fo', u'fos')) - buf = StringIO() - pofile.write_po(buf, catalog, omit_header=True, include_previous=True) - self.assertEqual('''#: main.py:1 -#| msgid "fo" -#| msgid_plural "fos" -msgid "foo" -msgid_plural "foos" -msgstr[0] "Voh" -msgstr[1] "Voeh"''', buf.getvalue().strip()) - - def test_sorted_po(self): - catalog = Catalog() - catalog.add(u'bar', locations=[('utils.py', 3)], - user_comments=['Comment About `bar` with', - 'multiple lines.']) - catalog.add((u'foo', u'foos'), (u'Voh', u'Voeh'), - locations=[('main.py', 1)]) - buf = StringIO() - pofile.write_po(buf, catalog, sort_output=True) - value = buf.getvalue().strip() - assert '''\ -# Comment About `bar` with -# multiple lines. -#: utils.py:3 -msgid "bar" -msgstr "" - -#: main.py:1 -msgid "foo" -msgid_plural "foos" -msgstr[0] "Voh" -msgstr[1] "Voeh"''' in value - assert value.find('msgid ""') < value.find('msgid "bar"') < value.find('msgid "foo"') - - def test_silent_location_fallback(self): - buf = StringIO('''\ -#: broken_file.py -msgid "missing line number" -msgstr "" - -#: broken_file.py:broken_line_number -msgid "broken line number" -msgstr ""''') - catalog = pofile.read_po(buf) - self.assertEqual(catalog['missing line number'].locations, []) - self.assertEqual(catalog['broken line number'].locations, []) - -def suite(): - suite = unittest.TestSuite() - is_py23 = sys.version_info[0:2] == (2, 3) - if not is_py23: - suite.addTest(doctest.DocTestSuite(pofile)) - suite.addTest(unittest.makeSuite(ReadPoTestCase)) - suite.addTest(unittest.makeSuite(WritePoTestCase)) - return suite - -if __name__ == '__main__': - unittest.main(defaultTest='suite') +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007-2010 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/. + +from datetime import datetime +import doctest +from StringIO import StringIO +import unittest +import sys + +from babel.messages.catalog import Catalog, Message +from babel.messages import pofile +from babel.util import FixedOffsetTimezone, LOCALTZ + + +class ReadPoTestCase(unittest.TestCase): + + def test_preserve_locale(self): + buf = StringIO(r'''msgid "foo" +msgstr "Voh"''') + catalog = pofile.read_po(buf, locale='en_US') + self.assertEqual('en_US', catalog.locale) + + def test_preserve_domain(self): + buf = StringIO(r'''msgid "foo" +msgstr "Voh"''') + catalog = pofile.read_po(buf, domain='mydomain') + self.assertEqual('mydomain', catalog.domain) + + def test_read_multiline(self): + buf = StringIO(r'''msgid "" +"Here's some text that\n" +"includesareallylongwordthatmightbutshouldnt" +" throw us into an infinite " +"loop\n" +msgstr ""''') + catalog = pofile.read_po(buf) + self.assertEqual(1, len(catalog)) + message = list(catalog)[1] + self.assertEqual("Here's some text that\nincludesareallylongwordthat" + "mightbutshouldnt throw us into an infinite loop\n", + message.id) + + def test_fuzzy_header(self): + buf = StringIO(r'''\ +# Translations template for AReallyReallyLongNameForAProject. +# Copyright (C) 2007 ORGANIZATION +# This file is distributed under the same license as the +# AReallyReallyLongNameForAProject project. +# FIRST AUTHOR , 2007. +# +#, fuzzy +''') + catalog = pofile.read_po(buf) + self.assertEqual(1, len(list(catalog))) + self.assertEqual(True, list(catalog)[0].fuzzy) + + def test_not_fuzzy_header(self): + buf = StringIO(r'''\ +# Translations template for AReallyReallyLongNameForAProject. +# Copyright (C) 2007 ORGANIZATION +# This file is distributed under the same license as the +# AReallyReallyLongNameForAProject project. +# FIRST AUTHOR , 2007. +# +''') + catalog = pofile.read_po(buf) + self.assertEqual(1, len(list(catalog))) + self.assertEqual(False, list(catalog)[0].fuzzy) + + def test_header_entry(self): + buf = StringIO(r'''\ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2007 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , 2007. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: 3.15\n" +"Report-Msgid-Bugs-To: Fliegender Zirkus \n" +"POT-Creation-Date: 2007-09-27 11:19+0700\n" +"PO-Revision-Date: 2007-09-27 21:42-0700\n" +"Last-Translator: John \n" +"Language-Team: German Lang \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=iso-8859-2\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.0dev-r313\n" +''') + catalog = pofile.read_po(buf) + self.assertEqual(1, len(list(catalog))) + self.assertEqual(u'3.15', catalog.version) + self.assertEqual(u'Fliegender Zirkus ', + catalog.msgid_bugs_address) + self.assertEqual(datetime(2007, 9, 27, 11, 19, + tzinfo=FixedOffsetTimezone(7 * 60)), + catalog.creation_date) + self.assertEqual(u'John ', catalog.last_translator) + self.assertEqual(u'German Lang ', catalog.language_team) + self.assertEqual(u'iso-8859-2', catalog.charset) + self.assertEqual(True, list(catalog)[0].fuzzy) + + def test_obsolete_message(self): + buf = StringIO(r'''# This is an obsolete message +#~ msgid "foo" +#~ msgstr "Voh" + +# This message is not obsolete +#: main.py:1 +msgid "bar" +msgstr "Bahr" +''') + catalog = pofile.read_po(buf) + self.assertEqual(1, len(catalog)) + self.assertEqual(1, len(catalog.obsolete)) + message = catalog.obsolete[u'foo'] + self.assertEqual(u'foo', message.id) + self.assertEqual(u'Voh', message.string) + self.assertEqual(['This is an obsolete message'], message.user_comments) + + def test_obsolete_message_ignored(self): + buf = StringIO(r'''# This is an obsolete message +#~ msgid "foo" +#~ msgstr "Voh" + +# This message is not obsolete +#: main.py:1 +msgid "bar" +msgstr "Bahr" +''') + catalog = pofile.read_po(buf, ignore_obsolete=True) + self.assertEqual(1, len(catalog)) + self.assertEqual(0, len(catalog.obsolete)) + + def test_single_plural_form(self): + buf = StringIO(r'''msgid "foo" +msgid_plural "foos" +msgstr[0] "Voh"''') + catalog = pofile.read_po(buf, locale='ja_JP') + self.assertEqual(1, len(catalog)) + self.assertEqual(1, catalog.num_plurals) + message = catalog['foo'] + self.assertEqual(1, len(message.string)) + + def test_singular_plural_form(self): + buf = StringIO(r'''msgid "foo" +msgid_plural "foos" +msgstr[0] "Voh" +msgstr[1] "Vohs"''') + catalog = pofile.read_po(buf, locale='nl_NL') + self.assertEqual(1, len(catalog)) + self.assertEqual(2, catalog.num_plurals) + message = catalog['foo'] + self.assertEqual(2, len(message.string)) + + def test_more_than_two_plural_forms(self): + buf = StringIO(r'''msgid "foo" +msgid_plural "foos" +msgstr[0] "Voh" +msgstr[1] "Vohs" +msgstr[2] "Vohss"''') + catalog = pofile.read_po(buf, locale='lv_LV') + self.assertEqual(1, len(catalog)) + self.assertEqual(3, catalog.num_plurals) + message = catalog['foo'] + self.assertEqual(3, len(message.string)) + self.assertEqual(u'Vohss', message.string[2]) + + def test_plural_with_square_brackets(self): + buf = StringIO(r'''msgid "foo" +msgid_plural "foos" +msgstr[0] "Voh [text]" +msgstr[1] "Vohs [text]"''') + catalog = pofile.read_po(buf, locale='nb_NO') + self.assertEqual(1, len(catalog)) + self.assertEqual(2, catalog.num_plurals) + message = catalog['foo'] + self.assertEqual(2, len(message.string)) + + +class WritePoTestCase(unittest.TestCase): + + def test_join_locations(self): + catalog = Catalog() + catalog.add(u'foo', locations=[('main.py', 1)]) + catalog.add(u'foo', locations=[('utils.py', 3)]) + buf = StringIO() + pofile.write_po(buf, catalog, omit_header=True) + self.assertEqual('''#: main.py:1 utils.py:3 +msgid "foo" +msgstr ""''', buf.getvalue().strip()) + + def test_duplicate_comments(self): + catalog = Catalog() + catalog.add(u'foo', auto_comments=['A comment']) + catalog.add(u'foo', auto_comments=['A comment']) + buf = StringIO() + pofile.write_po(buf, catalog, omit_header=True) + self.assertEqual('''#. A comment +msgid "foo" +msgstr ""''', buf.getvalue().strip()) + + def test_wrap_long_lines(self): + text = """Here's some text where +white space and line breaks matter, and should + +not be removed + +""" + catalog = Catalog() + catalog.add(text, locations=[('main.py', 1)]) + buf = StringIO() + pofile.write_po(buf, catalog, no_location=True, omit_header=True, + width=42) + self.assertEqual(r'''msgid "" +"Here's some text where \n" +"white space and line breaks matter, and" +" should\n" +"\n" +"not be removed\n" +"\n" +msgstr ""''', buf.getvalue().strip()) + + def test_wrap_long_lines_with_long_word(self): + text = """Here's some text that +includesareallylongwordthatmightbutshouldnt throw us into an infinite loop +""" + catalog = Catalog() + catalog.add(text, locations=[('main.py', 1)]) + buf = StringIO() + pofile.write_po(buf, catalog, no_location=True, omit_header=True, + width=32) + self.assertEqual(r'''msgid "" +"Here's some text that\n" +"includesareallylongwordthatmightbutshouldnt" +" throw us into an infinite " +"loop\n" +msgstr ""''', buf.getvalue().strip()) + + def test_wrap_long_lines_in_header(self): + """ + Verify that long lines in the header comment are wrapped correctly. + """ + catalog = Catalog(project='AReallyReallyLongNameForAProject', + revision_date=datetime(2007, 4, 1)) + buf = StringIO() + pofile.write_po(buf, catalog) + self.assertEqual('''\ +# Translations template for AReallyReallyLongNameForAProject. +# Copyright (C) 2007 ORGANIZATION +# This file is distributed under the same license as the +# AReallyReallyLongNameForAProject project. +# FIRST AUTHOR , 2007. +# +#, fuzzy''', '\n'.join(buf.getvalue().splitlines()[:7])) + + def test_wrap_locations_with_hyphens(self): + catalog = Catalog() + catalog.add(u'foo', locations=[ + ('doupy/templates/base/navmenu.inc.html.py', 60) + ]) + catalog.add(u'foo', locations=[ + ('doupy/templates/job-offers/helpers.html', 22) + ]) + buf = StringIO() + pofile.write_po(buf, catalog, omit_header=True) + self.assertEqual('''#: doupy/templates/base/navmenu.inc.html.py:60 +#: doupy/templates/job-offers/helpers.html:22 +msgid "foo" +msgstr ""''', buf.getvalue().strip()) + + def test_no_wrap_and_width_behaviour_on_comments(self): + catalog = Catalog() + catalog.add("Pretty dam long message id, which must really be big " + "to test this wrap behaviour, if not it won't work.", + locations=[("fake.py", n) for n in range(1, 30)]) + buf = StringIO() + pofile.write_po(buf, catalog, width=None, omit_header=True) + self.assertEqual("""\ +#: fake.py:1 fake.py:2 fake.py:3 fake.py:4 fake.py:5 fake.py:6 fake.py:7 +#: fake.py:8 fake.py:9 fake.py:10 fake.py:11 fake.py:12 fake.py:13 fake.py:14 +#: fake.py:15 fake.py:16 fake.py:17 fake.py:18 fake.py:19 fake.py:20 fake.py:21 +#: fake.py:22 fake.py:23 fake.py:24 fake.py:25 fake.py:26 fake.py:27 fake.py:28 +#: fake.py:29 +msgid "pretty dam long message id, which must really be big to test this wrap behaviour, if not it won't work." +msgstr "" + +""", buf.getvalue().lower()) + buf = StringIO() + pofile.write_po(buf, catalog, width=100, omit_header=True) + self.assertEqual("""\ +#: fake.py:1 fake.py:2 fake.py:3 fake.py:4 fake.py:5 fake.py:6 fake.py:7 fake.py:8 fake.py:9 fake.py:10 +#: fake.py:11 fake.py:12 fake.py:13 fake.py:14 fake.py:15 fake.py:16 fake.py:17 fake.py:18 fake.py:19 +#: fake.py:20 fake.py:21 fake.py:22 fake.py:23 fake.py:24 fake.py:25 fake.py:26 fake.py:27 fake.py:28 +#: fake.py:29 +msgid "" +"pretty dam long message id, which must really be big to test this wrap behaviour, if not it won't" +" work." +msgstr "" + +""", buf.getvalue().lower()) + + def test_pot_with_translator_comments(self): + catalog = Catalog() + catalog.add(u'foo', locations=[('main.py', 1)], + auto_comments=['Comment About `foo`']) + catalog.add(u'bar', locations=[('utils.py', 3)], + user_comments=['Comment About `bar` with', + 'multiple lines.']) + buf = StringIO() + pofile.write_po(buf, catalog, omit_header=True) + self.assertEqual('''#. Comment About `foo` +#: main.py:1 +msgid "foo" +msgstr "" + +# Comment About `bar` with +# multiple lines. +#: utils.py:3 +msgid "bar" +msgstr ""''', buf.getvalue().strip()) + + def test_po_with_obsolete_message(self): + catalog = Catalog() + catalog.add(u'foo', u'Voh', locations=[('main.py', 1)]) + catalog.obsolete['bar'] = Message(u'bar', u'Bahr', + locations=[('utils.py', 3)], + user_comments=['User comment']) + buf = StringIO() + pofile.write_po(buf, catalog, omit_header=True) + self.assertEqual('''#: main.py:1 +msgid "foo" +msgstr "Voh" + +# User comment +#~ msgid "bar" +#~ msgstr "Bahr"''', buf.getvalue().strip()) + + def test_po_with_multiline_obsolete_message(self): + catalog = Catalog() + catalog.add(u'foo', u'Voh', locations=[('main.py', 1)]) + msgid = r"""Here's a message that covers +multiple lines, and should still be handled +correctly. +""" + msgstr = r"""Here's a message that covers +multiple lines, and should still be handled +correctly. +""" + catalog.obsolete[msgid] = Message(msgid, msgstr, + locations=[('utils.py', 3)]) + buf = StringIO() + pofile.write_po(buf, catalog, omit_header=True) + self.assertEqual(r'''#: main.py:1 +msgid "foo" +msgstr "Voh" + +#~ msgid "" +#~ "Here's a message that covers\n" +#~ "multiple lines, and should still be handled\n" +#~ "correctly.\n" +#~ msgstr "" +#~ "Here's a message that covers\n" +#~ "multiple lines, and should still be handled\n" +#~ "correctly.\n"''', buf.getvalue().strip()) + + def test_po_with_obsolete_message_ignored(self): + catalog = Catalog() + catalog.add(u'foo', u'Voh', locations=[('main.py', 1)]) + catalog.obsolete['bar'] = Message(u'bar', u'Bahr', + locations=[('utils.py', 3)], + user_comments=['User comment']) + buf = StringIO() + pofile.write_po(buf, catalog, omit_header=True, ignore_obsolete=True) + self.assertEqual('''#: main.py:1 +msgid "foo" +msgstr "Voh"''', buf.getvalue().strip()) + + def test_po_with_previous_msgid(self): + catalog = Catalog() + catalog.add(u'foo', u'Voh', locations=[('main.py', 1)], + previous_id=u'fo') + buf = StringIO() + pofile.write_po(buf, catalog, omit_header=True, include_previous=True) + self.assertEqual('''#: main.py:1 +#| msgid "fo" +msgid "foo" +msgstr "Voh"''', buf.getvalue().strip()) + + def test_po_with_previous_msgid_plural(self): + catalog = Catalog() + catalog.add((u'foo', u'foos'), (u'Voh', u'Voeh'), + locations=[('main.py', 1)], previous_id=(u'fo', u'fos')) + buf = StringIO() + pofile.write_po(buf, catalog, omit_header=True, include_previous=True) + self.assertEqual('''#: main.py:1 +#| msgid "fo" +#| msgid_plural "fos" +msgid "foo" +msgid_plural "foos" +msgstr[0] "Voh" +msgstr[1] "Voeh"''', buf.getvalue().strip()) + + def test_sorted_po(self): + catalog = Catalog() + catalog.add(u'bar', locations=[('utils.py', 3)], + user_comments=['Comment About `bar` with', + 'multiple lines.']) + catalog.add((u'foo', u'foos'), (u'Voh', u'Voeh'), + locations=[('main.py', 1)]) + buf = StringIO() + pofile.write_po(buf, catalog, sort_output=True) + value = buf.getvalue().strip() + assert '''\ +# Comment About `bar` with +# multiple lines. +#: utils.py:3 +msgid "bar" +msgstr "" + +#: main.py:1 +msgid "foo" +msgid_plural "foos" +msgstr[0] "Voh" +msgstr[1] "Voeh"''' in value + assert value.find('msgid ""') < value.find('msgid "bar"') < value.find('msgid "foo"') + + def test_silent_location_fallback(self): + buf = StringIO('''\ +#: broken_file.py +msgid "missing line number" +msgstr "" + +#: broken_file.py:broken_line_number +msgid "broken line number" +msgstr ""''') + catalog = pofile.read_po(buf) + self.assertEqual(catalog['missing line number'].locations, []) + self.assertEqual(catalog['broken line number'].locations, []) + +def suite(): + suite = unittest.TestSuite() + is_py23 = sys.version_info[0:2] == (2, 3) + if not is_py23: + suite.addTest(doctest.DocTestSuite(pofile)) + suite.addTest(unittest.makeSuite(ReadPoTestCase)) + suite.addTest(unittest.makeSuite(WritePoTestCase)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/justice/external/babel/numbers.py b/justice/external/babel/numbers.py index 479ddec..64489f6 100644 --- a/justice/external/babel/numbers.py +++ b/justice/external/babel/numbers.py @@ -1,584 +1,584 @@ -# -*- 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: -# Padding and rounding increments in pattern: -# - http://www.unicode.org/reports/tr35/ (Appendix G.6) -import math -import re -try: - from decimal import Decimal - have_decimal = True -except ImportError: - have_decimal = False - -from babel.core import default_locale, Locale -from babel.util import rsplit - -__all__ = ['format_number', 'format_decimal', 'format_currency', - 'format_percent', 'format_scientific', 'parse_number', - 'parse_decimal', 'NumberFormatError'] -__docformat__ = 'restructuredtext en' - -LC_NUMERIC = default_locale('LC_NUMERIC') - -def get_currency_name(currency, locale=LC_NUMERIC): - """Return the name used by the locale for the specified currency. - - >>> get_currency_name('USD', 'en_US') - u'US Dollar' - - :param currency: the currency code - :param locale: the `Locale` object or locale identifier - :return: the currency symbol - :rtype: `unicode` - :since: version 0.9.4 - """ - return Locale.parse(locale).currencies.get(currency, currency) - -def get_currency_symbol(currency, locale=LC_NUMERIC): - """Return the symbol used by the locale for the specified currency. - - >>> get_currency_symbol('USD', 'en_US') - u'$' - - :param currency: the currency code - :param locale: the `Locale` object or locale identifier - :return: the currency symbol - :rtype: `unicode` - """ - return Locale.parse(locale).currency_symbols.get(currency, currency) - -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_plus_sign_symbol(locale=LC_NUMERIC): - """Return the plus sign symbol used by the current locale. - - >>> get_plus_sign_symbol('en_US') - u'+' - - :param locale: the `Locale` object or locale identifier - :return: the plus sign symbol - :rtype: `unicode` - """ - return Locale.parse(locale).number_symbols.get('plusSign', u'+') - -def get_minus_sign_symbol(locale=LC_NUMERIC): - """Return the plus sign symbol used by the current locale. - - >>> get_minus_sign_symbol('en_US') - u'-' - - :param locale: the `Locale` object or locale identifier - :return: the plus sign symbol - :rtype: `unicode` - """ - return Locale.parse(locale).number_symbols.get('minusSign', u'-') - -def get_exponential_symbol(locale=LC_NUMERIC): - """Return the symbol used by the locale to separate mantissa and exponent. - - >>> get_exponential_symbol('en_US') - u'E' - - :param locale: the `Locale` object or locale identifier - :return: the exponential symbol - :rtype: `unicode` - """ - return Locale.parse(locale).number_symbols.get('exponential', u'E') - -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): - """Return 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` - """ - # Do we really need this one? - return format_decimal(number, locale=locale) - -def format_decimal(number, format=None, locale=LC_NUMERIC): - """Return the given decimal number formatted for a specific locale. - - >>> format_decimal(1.2345, locale='en_US') - u'1.234' - >>> format_decimal(1.2346, locale='en_US') - u'1.235' - >>> format_decimal(-1.2346, locale='en_US') - u'-1.235' - >>> format_decimal(1.2345, locale='sv_SE') - u'1,234' - >>> format_decimal(12345, locale='de') - u'12.345' - - The appropriate thousands grouping and the decimal separator are used for - each locale: - - >>> format_decimal(12345.5, locale='en_US') - u'12,345.5' - - :param number: the number to format - :param format: - :param locale: the `Locale` object or locale identifier - :return: the formatted decimal number - :rtype: `unicode` - """ - locale = Locale.parse(locale) - if not format: - format = locale.decimal_formats.get(format) - pattern = parse_pattern(format) - return pattern.apply(number, locale) - -def format_currency(number, currency, format=None, locale=LC_NUMERIC): - u"""Return formatted currency value. - - >>> format_currency(1099.98, 'USD', locale='en_US') - u'$1,099.98' - >>> format_currency(1099.98, 'USD', locale='es_CO') - u'US$\\xa01.099,98' - >>> format_currency(1099.98, 'EUR', locale='de_DE') - u'1.099,98\\xa0\\u20ac' - - The pattern can also be specified explicitly: - - >>> format_currency(1099.98, 'EUR', u'\xa4\xa4 #,##0.00', locale='en_US') - u'EUR 1,099.98' - - :param number: the number to format - :param currency: the currency code - :param locale: the `Locale` object or locale identifier - :return: the formatted currency value - :rtype: `unicode` - """ - locale = Locale.parse(locale) - if not format: - format = locale.currency_formats.get(format) - pattern = parse_pattern(format) - return pattern.apply(number, locale, currency=currency) - -def format_percent(number, format=None, locale=LC_NUMERIC): - """Return formatted percent value for a specific locale. - - >>> format_percent(0.34, locale='en_US') - u'34%' - >>> format_percent(25.1234, locale='en_US') - u'2,512%' - >>> format_percent(25.1234, locale='sv_SE') - u'2\\xa0512\\xa0%' - - The format pattern can also be specified explicitly: - - >>> format_percent(25.1234, u'#,##0\u2030', locale='en_US') - u'25,123\u2030' - - :param number: the percent number to format - :param format: - :param locale: the `Locale` object or locale identifier - :return: the formatted percent number - :rtype: `unicode` - """ - locale = Locale.parse(locale) - if not format: - format = locale.percent_formats.get(format) - pattern = parse_pattern(format) - return pattern.apply(number, locale) - -def format_scientific(number, format=None, locale=LC_NUMERIC): - """Return value formatted in scientific notation for a specific locale. - - >>> format_scientific(10000, locale='en_US') - u'1E4' - - The format pattern can also be specified explicitly: - - >>> format_scientific(1234567, u'##0E00', locale='en_US') - u'1.23E06' - - :param number: the number to format - :param format: - :param locale: the `Locale` object or locale identifier - :return: value formatted in scientific notation. - :rtype: `unicode` - """ - locale = Locale.parse(locale) - if not format: - format = locale.scientific_formats.get(format) - pattern = parse_pattern(format) - return pattern.apply(number, locale) - - -class NumberFormatError(ValueError): - """Exception raised when a string cannot be parsed into a number.""" - - -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 - - When the given string cannot be parsed, an exception is raised: - - >>> parse_number('1.099,98', locale='de') - Traceback (most recent call last): - ... - NumberFormatError: '1.099,98' is not a valid number - - :param string: the string to parse - :param locale: the `Locale` object or locale identifier - :return: the parsed number - :rtype: `long` - :raise `NumberFormatError`: if the string can not be converted to a number - """ - try: - return long(string.replace(get_group_symbol(locale), '')) - except ValueError: - raise NumberFormatError('%r is not a valid number' % string) - -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') - 1099.98 - - When the given string cannot be parsed, an exception is raised: - - >>> parse_decimal('2,109,998', locale='de') - Traceback (most recent call last): - ... - NumberFormatError: '2,109,998' is not a valid decimal number - - :param string: the string to parse - :param locale: the `Locale` object or locale identifier - :return: the parsed decimal number - :rtype: `float` - :raise `NumberFormatError`: if the string can not be converted to a - decimal number - """ - locale = Locale.parse(locale) - try: - return float(string.replace(get_group_symbol(locale), '') - .replace(get_decimal_symbol(locale), '.')) - except ValueError: - raise NumberFormatError('%r is not a valid decimal number' % string) - - -PREFIX_END = r'[^0-9@#.,]' -NUMBER_TOKEN = r'[0-9@#.\-,E+]' - -PREFIX_PATTERN = r"(?P(?:'[^']*'|%s)*)" % PREFIX_END -NUMBER_PATTERN = r"(?P%s+)" % NUMBER_TOKEN -SUFFIX_PATTERN = r"(?P.*)" - -number_re = re.compile(r"%s%s%s" % (PREFIX_PATTERN, NUMBER_PATTERN, - SUFFIX_PATTERN)) - -def split_number(value): - """Convert a number into a (intasstring, fractionasstring) tuple""" - if have_decimal and isinstance(value, Decimal): - text = str(value) - else: - text = ('%.9f' % value).rstrip('0') - if '.' in text: - a, b = text.split('.', 1) - if b == '0': - b = '' - else: - a, b = text, '' - return a, b - -def bankersround(value, ndigits=0): - """Round a number to a given precision. - - Works like round() except that the round-half-even (banker's rounding) - algorithm is used instead of round-half-up. - - >>> bankersround(5.5, 0) - 6.0 - >>> bankersround(6.5, 0) - 6.0 - >>> bankersround(-6.5, 0) - -6.0 - >>> bankersround(1234.0, -2) - 1200.0 - """ - sign = int(value < 0) and -1 or 1 - value = abs(value) - a, b = split_number(value) - digits = a + b - add = 0 - i = len(a) + ndigits - if i < 0 or i >= len(digits): - pass - elif digits[i] > '5': - add = 1 - elif digits[i] == '5' and digits[i-1] in '13579': - add = 1 - scale = 10**ndigits - if have_decimal and isinstance(value, Decimal): - return Decimal(int(value * scale + add)) / scale * sign - else: - return float(int(value * scale + add)) / scale * sign - -def parse_pattern(pattern): - """Parse number format patterns""" - if isinstance(pattern, NumberPattern): - return pattern - - # Do we have a negative subpattern? - if ';' in pattern: - pattern, neg_pattern = pattern.split(';', 1) - pos_prefix, number, pos_suffix = number_re.search(pattern).groups() - neg_prefix, _, neg_suffix = number_re.search(neg_pattern).groups() - else: - pos_prefix, number, pos_suffix = number_re.search(pattern).groups() - neg_prefix = '-' + pos_prefix - neg_suffix = pos_suffix - if 'E' in number: - number, exp = number.split('E', 1) - else: - exp = None - if '@' in number: - if '.' in number and '0' in number: - raise ValueError('Significant digit patterns can not contain ' - '"@" or "0"') - if '.' in number: - integer, fraction = rsplit(number, '.', 1) - else: - integer = number - fraction = '' - min_frac = max_frac = 0 - - def parse_precision(p): - """Calculate the min and max allowed digits""" - min = max = 0 - for c in p: - if c in '@0': - min += 1 - max += 1 - elif c == '#': - max += 1 - elif c == ',': - continue - else: - break - return min, max - - def parse_grouping(p): - """Parse primary and secondary digit grouping - - >>> parse_grouping('##') - 0, 0 - >>> parse_grouping('#,###') - 3, 3 - >>> parse_grouping('#,####,###') - 3, 4 - """ - width = len(p) - g1 = p.rfind(',') - if g1 == -1: - return 1000, 1000 - g1 = width - g1 - 1 - g2 = p[:-g1 - 1].rfind(',') - if g2 == -1: - return g1, g1 - g2 = width - g1 - g2 - 2 - return g1, g2 - - int_prec = parse_precision(integer) - frac_prec = parse_precision(fraction) - if exp: - frac_prec = parse_precision(integer+fraction) - exp_plus = exp.startswith('+') - exp = exp.lstrip('+') - exp_prec = parse_precision(exp) - else: - exp_plus = None - exp_prec = None - grouping = parse_grouping(integer) - return NumberPattern(pattern, (pos_prefix, neg_prefix), - (pos_suffix, neg_suffix), grouping, - int_prec, frac_prec, - exp_prec, exp_plus) - - -class NumberPattern(object): - - def __init__(self, pattern, prefix, suffix, grouping, - int_prec, frac_prec, exp_prec, exp_plus): - self.pattern = pattern - self.prefix = prefix - self.suffix = suffix - self.grouping = grouping - self.int_prec = int_prec - self.frac_prec = frac_prec - self.exp_prec = exp_prec - self.exp_plus = exp_plus - if '%' in ''.join(self.prefix + self.suffix): - self.scale = 100 - elif u'‰' in ''.join(self.prefix + self.suffix): - self.scale = 1000 - else: - self.scale = 1 - - def __repr__(self): - return '<%s %r>' % (type(self).__name__, self.pattern) - - def apply(self, value, locale, currency=None): - value *= self.scale - is_negative = int(value < 0) - if self.exp_prec: # Scientific notation - value = abs(value) - if value: - exp = int(math.floor(math.log(value, 10))) - else: - exp = 0 - # Minimum number of integer digits - if self.int_prec[0] == self.int_prec[1]: - exp -= self.int_prec[0] - 1 - # Exponent grouping - elif self.int_prec[1]: - exp = int(exp) / self.int_prec[1] * self.int_prec[1] - if not have_decimal or not isinstance(value, Decimal): - value = float(value) - if exp < 0: - value = value * 10**(-exp) - else: - value = value / 10**exp - exp_sign = '' - if exp < 0: - exp_sign = get_minus_sign_symbol(locale) - elif self.exp_plus: - exp_sign = get_plus_sign_symbol(locale) - exp = abs(exp) - number = u'%s%s%s%s' % \ - (self._format_sigdig(value, self.frac_prec[0], - self.frac_prec[1]), - get_exponential_symbol(locale), exp_sign, - self._format_int(str(exp), self.exp_prec[0], - self.exp_prec[1], locale)) - elif '@' in self.pattern: # Is it a siginificant digits pattern? - text = self._format_sigdig(abs(value), - self.int_prec[0], - self.int_prec[1]) - if '.' in text: - a, b = text.split('.') - a = self._format_int(a, 0, 1000, locale) - if b: - b = get_decimal_symbol(locale) + b - number = a + b - else: - number = self._format_int(text, 0, 1000, locale) - else: # A normal number pattern - a, b = split_number(bankersround(abs(value), - self.frac_prec[1])) - b = b or '0' - a = self._format_int(a, self.int_prec[0], - self.int_prec[1], locale) - b = self._format_frac(b, locale) - number = a + b - retval = u'%s%s%s' % (self.prefix[is_negative], number, - self.suffix[is_negative]) - if u'¤' in retval: - retval = retval.replace(u'¤¤', currency.upper()) - retval = retval.replace(u'¤', get_currency_symbol(currency, locale)) - return retval - - def _format_sigdig(self, value, min, max): - """Convert value to a string. - - The resulting string will contain between (min, max) number of - significant digits. - """ - a, b = split_number(value) - ndecimals = len(a) - if a == '0' and b != '': - ndecimals = 0 - while b.startswith('0'): - b = b[1:] - ndecimals -= 1 - a, b = split_number(bankersround(value, max - ndecimals)) - digits = len((a + b).lstrip('0')) - if not digits: - digits = 1 - # Figure out if we need to add any trailing '0':s - if len(a) >= max and a != '0': - return a - if digits < min: - b += ('0' * (min - digits)) - if b: - return '%s.%s' % (a, b) - return a - - def _format_int(self, value, min, max, locale): - width = len(value) - if width < min: - value = '0' * (min - width) + value - gsize = self.grouping[0] - ret = '' - symbol = get_group_symbol(locale) - while len(value) > gsize: - ret = symbol + value[-gsize:] + ret - value = value[:-gsize] - gsize = self.grouping[1] - return value + ret - - def _format_frac(self, value, locale): - min, max = self.frac_prec - if len(value) < min: - value += ('0' * (min - len(value))) - if max == 0 or (min == 0 and int(value) == 0): - return '' - width = len(value) - while len(value) > min and value[-1] == '0': - value = value[:-1] - return get_decimal_symbol(locale) + value +# -*- 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: +# Padding and rounding increments in pattern: +# - http://www.unicode.org/reports/tr35/ (Appendix G.6) +import math +import re +try: + from decimal import Decimal + have_decimal = True +except ImportError: + have_decimal = False + +from babel.core import default_locale, Locale +from babel.util import rsplit + +__all__ = ['format_number', 'format_decimal', 'format_currency', + 'format_percent', 'format_scientific', 'parse_number', + 'parse_decimal', 'NumberFormatError'] +__docformat__ = 'restructuredtext en' + +LC_NUMERIC = default_locale('LC_NUMERIC') + +def get_currency_name(currency, locale=LC_NUMERIC): + """Return the name used by the locale for the specified currency. + + >>> get_currency_name('USD', 'en_US') + u'US Dollar' + + :param currency: the currency code + :param locale: the `Locale` object or locale identifier + :return: the currency symbol + :rtype: `unicode` + :since: version 0.9.4 + """ + return Locale.parse(locale).currencies.get(currency, currency) + +def get_currency_symbol(currency, locale=LC_NUMERIC): + """Return the symbol used by the locale for the specified currency. + + >>> get_currency_symbol('USD', 'en_US') + u'$' + + :param currency: the currency code + :param locale: the `Locale` object or locale identifier + :return: the currency symbol + :rtype: `unicode` + """ + return Locale.parse(locale).currency_symbols.get(currency, currency) + +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_plus_sign_symbol(locale=LC_NUMERIC): + """Return the plus sign symbol used by the current locale. + + >>> get_plus_sign_symbol('en_US') + u'+' + + :param locale: the `Locale` object or locale identifier + :return: the plus sign symbol + :rtype: `unicode` + """ + return Locale.parse(locale).number_symbols.get('plusSign', u'+') + +def get_minus_sign_symbol(locale=LC_NUMERIC): + """Return the plus sign symbol used by the current locale. + + >>> get_minus_sign_symbol('en_US') + u'-' + + :param locale: the `Locale` object or locale identifier + :return: the plus sign symbol + :rtype: `unicode` + """ + return Locale.parse(locale).number_symbols.get('minusSign', u'-') + +def get_exponential_symbol(locale=LC_NUMERIC): + """Return the symbol used by the locale to separate mantissa and exponent. + + >>> get_exponential_symbol('en_US') + u'E' + + :param locale: the `Locale` object or locale identifier + :return: the exponential symbol + :rtype: `unicode` + """ + return Locale.parse(locale).number_symbols.get('exponential', u'E') + +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): + """Return 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` + """ + # Do we really need this one? + return format_decimal(number, locale=locale) + +def format_decimal(number, format=None, locale=LC_NUMERIC): + """Return the given decimal number formatted for a specific locale. + + >>> format_decimal(1.2345, locale='en_US') + u'1.234' + >>> format_decimal(1.2346, locale='en_US') + u'1.235' + >>> format_decimal(-1.2346, locale='en_US') + u'-1.235' + >>> format_decimal(1.2345, locale='sv_SE') + u'1,234' + >>> format_decimal(12345, locale='de') + u'12.345' + + The appropriate thousands grouping and the decimal separator are used for + each locale: + + >>> format_decimal(12345.5, locale='en_US') + u'12,345.5' + + :param number: the number to format + :param format: + :param locale: the `Locale` object or locale identifier + :return: the formatted decimal number + :rtype: `unicode` + """ + locale = Locale.parse(locale) + if not format: + format = locale.decimal_formats.get(format) + pattern = parse_pattern(format) + return pattern.apply(number, locale) + +def format_currency(number, currency, format=None, locale=LC_NUMERIC): + u"""Return formatted currency value. + + >>> format_currency(1099.98, 'USD', locale='en_US') + u'$1,099.98' + >>> format_currency(1099.98, 'USD', locale='es_CO') + u'US$\\xa01.099,98' + >>> format_currency(1099.98, 'EUR', locale='de_DE') + u'1.099,98\\xa0\\u20ac' + + The pattern can also be specified explicitly: + + >>> format_currency(1099.98, 'EUR', u'\xa4\xa4 #,##0.00', locale='en_US') + u'EUR 1,099.98' + + :param number: the number to format + :param currency: the currency code + :param locale: the `Locale` object or locale identifier + :return: the formatted currency value + :rtype: `unicode` + """ + locale = Locale.parse(locale) + if not format: + format = locale.currency_formats.get(format) + pattern = parse_pattern(format) + return pattern.apply(number, locale, currency=currency) + +def format_percent(number, format=None, locale=LC_NUMERIC): + """Return formatted percent value for a specific locale. + + >>> format_percent(0.34, locale='en_US') + u'34%' + >>> format_percent(25.1234, locale='en_US') + u'2,512%' + >>> format_percent(25.1234, locale='sv_SE') + u'2\\xa0512\\xa0%' + + The format pattern can also be specified explicitly: + + >>> format_percent(25.1234, u'#,##0\u2030', locale='en_US') + u'25,123\u2030' + + :param number: the percent number to format + :param format: + :param locale: the `Locale` object or locale identifier + :return: the formatted percent number + :rtype: `unicode` + """ + locale = Locale.parse(locale) + if not format: + format = locale.percent_formats.get(format) + pattern = parse_pattern(format) + return pattern.apply(number, locale) + +def format_scientific(number, format=None, locale=LC_NUMERIC): + """Return value formatted in scientific notation for a specific locale. + + >>> format_scientific(10000, locale='en_US') + u'1E4' + + The format pattern can also be specified explicitly: + + >>> format_scientific(1234567, u'##0E00', locale='en_US') + u'1.23E06' + + :param number: the number to format + :param format: + :param locale: the `Locale` object or locale identifier + :return: value formatted in scientific notation. + :rtype: `unicode` + """ + locale = Locale.parse(locale) + if not format: + format = locale.scientific_formats.get(format) + pattern = parse_pattern(format) + return pattern.apply(number, locale) + + +class NumberFormatError(ValueError): + """Exception raised when a string cannot be parsed into a number.""" + + +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 + + When the given string cannot be parsed, an exception is raised: + + >>> parse_number('1.099,98', locale='de') + Traceback (most recent call last): + ... + NumberFormatError: '1.099,98' is not a valid number + + :param string: the string to parse + :param locale: the `Locale` object or locale identifier + :return: the parsed number + :rtype: `long` + :raise `NumberFormatError`: if the string can not be converted to a number + """ + try: + return long(string.replace(get_group_symbol(locale), '')) + except ValueError: + raise NumberFormatError('%r is not a valid number' % string) + +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') + 1099.98 + + When the given string cannot be parsed, an exception is raised: + + >>> parse_decimal('2,109,998', locale='de') + Traceback (most recent call last): + ... + NumberFormatError: '2,109,998' is not a valid decimal number + + :param string: the string to parse + :param locale: the `Locale` object or locale identifier + :return: the parsed decimal number + :rtype: `float` + :raise `NumberFormatError`: if the string can not be converted to a + decimal number + """ + locale = Locale.parse(locale) + try: + return float(string.replace(get_group_symbol(locale), '') + .replace(get_decimal_symbol(locale), '.')) + except ValueError: + raise NumberFormatError('%r is not a valid decimal number' % string) + + +PREFIX_END = r'[^0-9@#.,]' +NUMBER_TOKEN = r'[0-9@#.\-,E+]' + +PREFIX_PATTERN = r"(?P(?:'[^']*'|%s)*)" % PREFIX_END +NUMBER_PATTERN = r"(?P%s+)" % NUMBER_TOKEN +SUFFIX_PATTERN = r"(?P.*)" + +number_re = re.compile(r"%s%s%s" % (PREFIX_PATTERN, NUMBER_PATTERN, + SUFFIX_PATTERN)) + +def split_number(value): + """Convert a number into a (intasstring, fractionasstring) tuple""" + if have_decimal and isinstance(value, Decimal): + text = str(value) + else: + text = ('%.9f' % value).rstrip('0') + if '.' in text: + a, b = text.split('.', 1) + if b == '0': + b = '' + else: + a, b = text, '' + return a, b + +def bankersround(value, ndigits=0): + """Round a number to a given precision. + + Works like round() except that the round-half-even (banker's rounding) + algorithm is used instead of round-half-up. + + >>> bankersround(5.5, 0) + 6.0 + >>> bankersround(6.5, 0) + 6.0 + >>> bankersround(-6.5, 0) + -6.0 + >>> bankersround(1234.0, -2) + 1200.0 + """ + sign = int(value < 0) and -1 or 1 + value = abs(value) + a, b = split_number(value) + digits = a + b + add = 0 + i = len(a) + ndigits + if i < 0 or i >= len(digits): + pass + elif digits[i] > '5': + add = 1 + elif digits[i] == '5' and digits[i-1] in '13579': + add = 1 + scale = 10**ndigits + if have_decimal and isinstance(value, Decimal): + return Decimal(int(value * scale + add)) / scale * sign + else: + return float(int(value * scale + add)) / scale * sign + +def parse_pattern(pattern): + """Parse number format patterns""" + if isinstance(pattern, NumberPattern): + return pattern + + # Do we have a negative subpattern? + if ';' in pattern: + pattern, neg_pattern = pattern.split(';', 1) + pos_prefix, number, pos_suffix = number_re.search(pattern).groups() + neg_prefix, _, neg_suffix = number_re.search(neg_pattern).groups() + else: + pos_prefix, number, pos_suffix = number_re.search(pattern).groups() + neg_prefix = '-' + pos_prefix + neg_suffix = pos_suffix + if 'E' in number: + number, exp = number.split('E', 1) + else: + exp = None + if '@' in number: + if '.' in number and '0' in number: + raise ValueError('Significant digit patterns can not contain ' + '"@" or "0"') + if '.' in number: + integer, fraction = rsplit(number, '.', 1) + else: + integer = number + fraction = '' + min_frac = max_frac = 0 + + def parse_precision(p): + """Calculate the min and max allowed digits""" + min = max = 0 + for c in p: + if c in '@0': + min += 1 + max += 1 + elif c == '#': + max += 1 + elif c == ',': + continue + else: + break + return min, max + + def parse_grouping(p): + """Parse primary and secondary digit grouping + + >>> parse_grouping('##') + 0, 0 + >>> parse_grouping('#,###') + 3, 3 + >>> parse_grouping('#,####,###') + 3, 4 + """ + width = len(p) + g1 = p.rfind(',') + if g1 == -1: + return 1000, 1000 + g1 = width - g1 - 1 + g2 = p[:-g1 - 1].rfind(',') + if g2 == -1: + return g1, g1 + g2 = width - g1 - g2 - 2 + return g1, g2 + + int_prec = parse_precision(integer) + frac_prec = parse_precision(fraction) + if exp: + frac_prec = parse_precision(integer+fraction) + exp_plus = exp.startswith('+') + exp = exp.lstrip('+') + exp_prec = parse_precision(exp) + else: + exp_plus = None + exp_prec = None + grouping = parse_grouping(integer) + return NumberPattern(pattern, (pos_prefix, neg_prefix), + (pos_suffix, neg_suffix), grouping, + int_prec, frac_prec, + exp_prec, exp_plus) + + +class NumberPattern(object): + + def __init__(self, pattern, prefix, suffix, grouping, + int_prec, frac_prec, exp_prec, exp_plus): + self.pattern = pattern + self.prefix = prefix + self.suffix = suffix + self.grouping = grouping + self.int_prec = int_prec + self.frac_prec = frac_prec + self.exp_prec = exp_prec + self.exp_plus = exp_plus + if '%' in ''.join(self.prefix + self.suffix): + self.scale = 100 + elif u'‰' in ''.join(self.prefix + self.suffix): + self.scale = 1000 + else: + self.scale = 1 + + def __repr__(self): + return '<%s %r>' % (type(self).__name__, self.pattern) + + def apply(self, value, locale, currency=None): + value *= self.scale + is_negative = int(value < 0) + if self.exp_prec: # Scientific notation + value = abs(value) + if value: + exp = int(math.floor(math.log(value, 10))) + else: + exp = 0 + # Minimum number of integer digits + if self.int_prec[0] == self.int_prec[1]: + exp -= self.int_prec[0] - 1 + # Exponent grouping + elif self.int_prec[1]: + exp = int(exp) / self.int_prec[1] * self.int_prec[1] + if not have_decimal or not isinstance(value, Decimal): + value = float(value) + if exp < 0: + value = value * 10**(-exp) + else: + value = value / 10**exp + exp_sign = '' + if exp < 0: + exp_sign = get_minus_sign_symbol(locale) + elif self.exp_plus: + exp_sign = get_plus_sign_symbol(locale) + exp = abs(exp) + number = u'%s%s%s%s' % \ + (self._format_sigdig(value, self.frac_prec[0], + self.frac_prec[1]), + get_exponential_symbol(locale), exp_sign, + self._format_int(str(exp), self.exp_prec[0], + self.exp_prec[1], locale)) + elif '@' in self.pattern: # Is it a siginificant digits pattern? + text = self._format_sigdig(abs(value), + self.int_prec[0], + self.int_prec[1]) + if '.' in text: + a, b = text.split('.') + a = self._format_int(a, 0, 1000, locale) + if b: + b = get_decimal_symbol(locale) + b + number = a + b + else: + number = self._format_int(text, 0, 1000, locale) + else: # A normal number pattern + a, b = split_number(bankersround(abs(value), + self.frac_prec[1])) + b = b or '0' + a = self._format_int(a, self.int_prec[0], + self.int_prec[1], locale) + b = self._format_frac(b, locale) + number = a + b + retval = u'%s%s%s' % (self.prefix[is_negative], number, + self.suffix[is_negative]) + if u'¤' in retval: + retval = retval.replace(u'¤¤', currency.upper()) + retval = retval.replace(u'¤', get_currency_symbol(currency, locale)) + return retval + + def _format_sigdig(self, value, min, max): + """Convert value to a string. + + The resulting string will contain between (min, max) number of + significant digits. + """ + a, b = split_number(value) + ndecimals = len(a) + if a == '0' and b != '': + ndecimals = 0 + while b.startswith('0'): + b = b[1:] + ndecimals -= 1 + a, b = split_number(bankersround(value, max - ndecimals)) + digits = len((a + b).lstrip('0')) + if not digits: + digits = 1 + # Figure out if we need to add any trailing '0':s + if len(a) >= max and a != '0': + return a + if digits < min: + b += ('0' * (min - digits)) + if b: + return '%s.%s' % (a, b) + return a + + def _format_int(self, value, min, max, locale): + width = len(value) + if width < min: + value = '0' * (min - width) + value + gsize = self.grouping[0] + ret = '' + symbol = get_group_symbol(locale) + while len(value) > gsize: + ret = symbol + value[-gsize:] + ret + value = value[:-gsize] + gsize = self.grouping[1] + return value + ret + + def _format_frac(self, value, locale): + min, max = self.frac_prec + if len(value) < min: + value += ('0' * (min - len(value))) + if max == 0 or (min == 0 and int(value) == 0): + return '' + width = len(value) + while len(value) > min and value[-1] == '0': + value = value[:-1] + return get_decimal_symbol(locale) + value diff --git a/justice/external/babel/support.py b/justice/external/babel/support.py index c290932..296a3b7 100644 --- a/justice/external/babel/support.py +++ b/justice/external/babel/support.py @@ -1,393 +1,393 @@ -# -*- 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/. - -"""Several classes and functions that help with integrating and using Babel -in applications. - -.. note: the code in this module is not used by Babel itself -""" - -from datetime import date, datetime, time -import gettext - -from babel.core import Locale -from babel.dates import format_date, format_datetime, format_time, LC_TIME -from babel.numbers import format_number, format_decimal, format_currency, \ - format_percent, format_scientific, LC_NUMERIC -from babel.util import set, UTC - -__all__ = ['Format', 'LazyProxy', 'Translations'] -__docformat__ = 'restructuredtext en' - - -class Format(object): - """Wrapper class providing the various date and number formatting functions - bound to a specific locale and time-zone. - - >>> fmt = Format('en_US', UTC) - >>> fmt.date(date(2007, 4, 1)) - u'Apr 1, 2007' - >>> fmt.decimal(1.2345) - u'1.234' - """ - - def __init__(self, locale, tzinfo=None): - """Initialize the formatter. - - :param locale: the locale identifier or `Locale` instance - :param tzinfo: the time-zone info (a `tzinfo` instance or `None`) - """ - self.locale = Locale.parse(locale) - self.tzinfo = tzinfo - - def date(self, date=None, format='medium'): - """Return a date formatted according to the given pattern. - - >>> fmt = Format('en_US') - >>> fmt.date(date(2007, 4, 1)) - u'Apr 1, 2007' - - :see: `babel.dates.format_date` - """ - return format_date(date, format, locale=self.locale) - - def datetime(self, datetime=None, format='medium'): - """Return a date and time formatted according to the given pattern. - - >>> from pytz import timezone - >>> fmt = Format('en_US', tzinfo=timezone('US/Eastern')) - >>> fmt.datetime(datetime(2007, 4, 1, 15, 30)) - u'Apr 1, 2007 11:30:00 AM' - - :see: `babel.dates.format_datetime` - """ - return format_datetime(datetime, format, tzinfo=self.tzinfo, - locale=self.locale) - - def time(self, time=None, format='medium'): - """Return a time formatted according to the given pattern. - - >>> from pytz import timezone - >>> fmt = Format('en_US', tzinfo=timezone('US/Eastern')) - >>> fmt.time(datetime(2007, 4, 1, 15, 30)) - u'11:30:00 AM' - - :see: `babel.dates.format_time` - """ - return format_time(time, format, tzinfo=self.tzinfo, locale=self.locale) - - def number(self, number): - """Return an integer number formatted for the locale. - - >>> fmt = Format('en_US') - >>> fmt.number(1099) - u'1,099' - - :see: `babel.numbers.format_number` - """ - return format_number(number, locale=self.locale) - - def decimal(self, number, format=None): - """Return a decimal number formatted for the locale. - - >>> fmt = Format('en_US') - >>> fmt.decimal(1.2345) - u'1.234' - - :see: `babel.numbers.format_decimal` - """ - return format_decimal(number, format, locale=self.locale) - - def currency(self, number, currency): - """Return a number in the given currency formatted for the locale. - - :see: `babel.numbers.format_currency` - """ - return format_currency(number, currency, locale=self.locale) - - def percent(self, number, format=None): - """Return a number formatted as percentage for the locale. - - >>> fmt = Format('en_US') - >>> fmt.percent(0.34) - u'34%' - - :see: `babel.numbers.format_percent` - """ - return format_percent(number, format, locale=self.locale) - - def scientific(self, number): - """Return a number formatted using scientific notation for the locale. - - :see: `babel.numbers.format_scientific` - """ - return format_scientific(number, locale=self.locale) - - -class LazyProxy(object): - """Class for proxy objects that delegate to a specified function to evaluate - the actual object. - - >>> def greeting(name='world'): - ... return 'Hello, %s!' % name - >>> lazy_greeting = LazyProxy(greeting, name='Joe') - >>> print lazy_greeting - Hello, Joe! - >>> u' ' + lazy_greeting - u' Hello, Joe!' - >>> u'(%s)' % lazy_greeting - u'(Hello, Joe!)' - - This can be used, for example, to implement lazy translation functions that - delay the actual translation until the string is actually used. The - rationale for such behavior is that the locale of the user may not always - be available. In web applications, you only know the locale when processing - a request. - - The proxy implementation attempts to be as complete as possible, so that - the lazy objects should mostly work as expected, for example for sorting: - - >>> greetings = [ - ... LazyProxy(greeting, 'world'), - ... LazyProxy(greeting, 'Joe'), - ... LazyProxy(greeting, 'universe'), - ... ] - >>> greetings.sort() - >>> for greeting in greetings: - ... print greeting - Hello, Joe! - Hello, universe! - Hello, world! - """ - __slots__ = ['_func', '_args', '_kwargs', '_value'] - - def __init__(self, func, *args, **kwargs): - # Avoid triggering our own __setattr__ implementation - object.__setattr__(self, '_func', func) - object.__setattr__(self, '_args', args) - object.__setattr__(self, '_kwargs', kwargs) - object.__setattr__(self, '_value', None) - - def value(self): - if self._value is None: - value = self._func(*self._args, **self._kwargs) - object.__setattr__(self, '_value', value) - return self._value - value = property(value) - - def __contains__(self, key): - return key in self.value - - def __nonzero__(self): - return bool(self.value) - - def __dir__(self): - return dir(self.value) - - def __iter__(self): - return iter(self.value) - - def __len__(self): - return len(self.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 __lt__(self, other): - return self.value < other - - def __le__(self, other): - return self.value <= other - - def __eq__(self, other): - return self.value == other - - def __ne__(self, other): - return self.value != other - - def __gt__(self, other): - return self.value > other - - def __ge__(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[key] - - def __getitem__(self, key): - return self.value[key] - - def __setitem__(self, key, value): - self.value[key] = value - - -class Translations(gettext.GNUTranslations, object): - """An extended translation catalog class.""" - - DEFAULT_DOMAIN = 'messages' - - def __init__(self, fileobj=None, domain=DEFAULT_DOMAIN): - """Initialize the translations catalog. - - :param fileobj: the file-like object the translation should be read - from - """ - gettext.GNUTranslations.__init__(self, fp=fileobj) - self.files = filter(None, [getattr(fileobj, 'name', None)]) - self.domain = domain - self._domains = {} - - 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` - """ - if locales is not None: - if not isinstance(locales, (list, tuple)): - locales = [locales] - locales = [str(locale) for locale in locales] - if not domain: - domain = cls.DEFAULT_DOMAIN - filename = gettext.find(domain, dirname, locales) - if not filename: - return gettext.NullTranslations() - return cls(fileobj=open(filename, 'rb'), domain=domain) - load = classmethod(load) - - def __repr__(self): - return '<%s: "%s">' % (type(self).__name__, - self._info.get('project-id-version')) - - def add(self, translations, merge=True): - """Add the given translations to the catalog. - - If the domain of the translations is different than that of the - current catalog, they are added as a catalog that is only accessible - by the various ``d*gettext`` functions. - - :param translations: the `Translations` instance with the messages to - add - :param merge: whether translations for message domains that have - already been added should be merged with the existing - translations - :return: the `Translations` instance (``self``) so that `merge` calls - can be easily chained - :rtype: `Translations` - """ - domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN) - if merge and domain == self.domain: - return self.merge(translations) - - existing = self._domains.get(domain) - if merge and existing is not None: - existing.merge(translations) - else: - translations.add_fallback(self) - self._domains[domain] = translations - - return self - - def merge(self, translations): - """Merge the given translations into the catalog. - - Message translations in the specified 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, gettext.GNUTranslations): - self._catalog.update(translations._catalog) - if isinstance(translations, Translations): - self.files.extend(translations.files) - - return self - - def dgettext(self, domain, message): - """Like ``gettext()``, but look the message up in the specified - domain. - """ - return self._domains.get(domain, self).gettext(message) - - def ldgettext(self, domain, message): - """Like ``lgettext()``, but look the message up in the specified - domain. - """ - return self._domains.get(domain, self).lgettext(message) - - def dugettext(self, domain, message): - """Like ``ugettext()``, but look the message up in the specified - domain. - """ - return self._domains.get(domain, self).ugettext(message) - - def dngettext(self, domain, singular, plural, num): - """Like ``ngettext()``, but look the message up in the specified - domain. - """ - return self._domains.get(domain, self).ngettext(singular, plural, num) - - def ldngettext(self, domain, singular, plural, num): - """Like ``lngettext()``, but look the message up in the specified - domain. - """ - return self._domains.get(domain, self).lngettext(singular, plural, num) - - def dungettext(self, domain, singular, plural, num): - """Like ``ungettext()`` but look the message up in the specified - domain. - """ - return self._domains.get(domain, self).ungettext(singular, plural, num) +# -*- 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/. + +"""Several classes and functions that help with integrating and using Babel +in applications. + +.. note: the code in this module is not used by Babel itself +""" + +from datetime import date, datetime, time +import gettext + +from babel.core import Locale +from babel.dates import format_date, format_datetime, format_time, LC_TIME +from babel.numbers import format_number, format_decimal, format_currency, \ + format_percent, format_scientific, LC_NUMERIC +from babel.util import set, UTC + +__all__ = ['Format', 'LazyProxy', 'Translations'] +__docformat__ = 'restructuredtext en' + + +class Format(object): + """Wrapper class providing the various date and number formatting functions + bound to a specific locale and time-zone. + + >>> fmt = Format('en_US', UTC) + >>> fmt.date(date(2007, 4, 1)) + u'Apr 1, 2007' + >>> fmt.decimal(1.2345) + u'1.234' + """ + + def __init__(self, locale, tzinfo=None): + """Initialize the formatter. + + :param locale: the locale identifier or `Locale` instance + :param tzinfo: the time-zone info (a `tzinfo` instance or `None`) + """ + self.locale = Locale.parse(locale) + self.tzinfo = tzinfo + + def date(self, date=None, format='medium'): + """Return a date formatted according to the given pattern. + + >>> fmt = Format('en_US') + >>> fmt.date(date(2007, 4, 1)) + u'Apr 1, 2007' + + :see: `babel.dates.format_date` + """ + return format_date(date, format, locale=self.locale) + + def datetime(self, datetime=None, format='medium'): + """Return a date and time formatted according to the given pattern. + + >>> from pytz import timezone + >>> fmt = Format('en_US', tzinfo=timezone('US/Eastern')) + >>> fmt.datetime(datetime(2007, 4, 1, 15, 30)) + u'Apr 1, 2007 11:30:00 AM' + + :see: `babel.dates.format_datetime` + """ + return format_datetime(datetime, format, tzinfo=self.tzinfo, + locale=self.locale) + + def time(self, time=None, format='medium'): + """Return a time formatted according to the given pattern. + + >>> from pytz import timezone + >>> fmt = Format('en_US', tzinfo=timezone('US/Eastern')) + >>> fmt.time(datetime(2007, 4, 1, 15, 30)) + u'11:30:00 AM' + + :see: `babel.dates.format_time` + """ + return format_time(time, format, tzinfo=self.tzinfo, locale=self.locale) + + def number(self, number): + """Return an integer number formatted for the locale. + + >>> fmt = Format('en_US') + >>> fmt.number(1099) + u'1,099' + + :see: `babel.numbers.format_number` + """ + return format_number(number, locale=self.locale) + + def decimal(self, number, format=None): + """Return a decimal number formatted for the locale. + + >>> fmt = Format('en_US') + >>> fmt.decimal(1.2345) + u'1.234' + + :see: `babel.numbers.format_decimal` + """ + return format_decimal(number, format, locale=self.locale) + + def currency(self, number, currency): + """Return a number in the given currency formatted for the locale. + + :see: `babel.numbers.format_currency` + """ + return format_currency(number, currency, locale=self.locale) + + def percent(self, number, format=None): + """Return a number formatted as percentage for the locale. + + >>> fmt = Format('en_US') + >>> fmt.percent(0.34) + u'34%' + + :see: `babel.numbers.format_percent` + """ + return format_percent(number, format, locale=self.locale) + + def scientific(self, number): + """Return a number formatted using scientific notation for the locale. + + :see: `babel.numbers.format_scientific` + """ + return format_scientific(number, locale=self.locale) + + +class LazyProxy(object): + """Class for proxy objects that delegate to a specified function to evaluate + the actual object. + + >>> def greeting(name='world'): + ... return 'Hello, %s!' % name + >>> lazy_greeting = LazyProxy(greeting, name='Joe') + >>> print lazy_greeting + Hello, Joe! + >>> u' ' + lazy_greeting + u' Hello, Joe!' + >>> u'(%s)' % lazy_greeting + u'(Hello, Joe!)' + + This can be used, for example, to implement lazy translation functions that + delay the actual translation until the string is actually used. The + rationale for such behavior is that the locale of the user may not always + be available. In web applications, you only know the locale when processing + a request. + + The proxy implementation attempts to be as complete as possible, so that + the lazy objects should mostly work as expected, for example for sorting: + + >>> greetings = [ + ... LazyProxy(greeting, 'world'), + ... LazyProxy(greeting, 'Joe'), + ... LazyProxy(greeting, 'universe'), + ... ] + >>> greetings.sort() + >>> for greeting in greetings: + ... print greeting + Hello, Joe! + Hello, universe! + Hello, world! + """ + __slots__ = ['_func', '_args', '_kwargs', '_value'] + + def __init__(self, func, *args, **kwargs): + # Avoid triggering our own __setattr__ implementation + object.__setattr__(self, '_func', func) + object.__setattr__(self, '_args', args) + object.__setattr__(self, '_kwargs', kwargs) + object.__setattr__(self, '_value', None) + + def value(self): + if self._value is None: + value = self._func(*self._args, **self._kwargs) + object.__setattr__(self, '_value', value) + return self._value + value = property(value) + + def __contains__(self, key): + return key in self.value + + def __nonzero__(self): + return bool(self.value) + + def __dir__(self): + return dir(self.value) + + def __iter__(self): + return iter(self.value) + + def __len__(self): + return len(self.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 __lt__(self, other): + return self.value < other + + def __le__(self, other): + return self.value <= other + + def __eq__(self, other): + return self.value == other + + def __ne__(self, other): + return self.value != other + + def __gt__(self, other): + return self.value > other + + def __ge__(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[key] + + def __getitem__(self, key): + return self.value[key] + + def __setitem__(self, key, value): + self.value[key] = value + + +class Translations(gettext.GNUTranslations, object): + """An extended translation catalog class.""" + + DEFAULT_DOMAIN = 'messages' + + def __init__(self, fileobj=None, domain=DEFAULT_DOMAIN): + """Initialize the translations catalog. + + :param fileobj: the file-like object the translation should be read + from + """ + gettext.GNUTranslations.__init__(self, fp=fileobj) + self.files = filter(None, [getattr(fileobj, 'name', None)]) + self.domain = domain + self._domains = {} + + 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` + """ + if locales is not None: + if not isinstance(locales, (list, tuple)): + locales = [locales] + locales = [str(locale) for locale in locales] + if not domain: + domain = cls.DEFAULT_DOMAIN + filename = gettext.find(domain, dirname, locales) + if not filename: + return gettext.NullTranslations() + return cls(fileobj=open(filename, 'rb'), domain=domain) + load = classmethod(load) + + def __repr__(self): + return '<%s: "%s">' % (type(self).__name__, + self._info.get('project-id-version')) + + def add(self, translations, merge=True): + """Add the given translations to the catalog. + + If the domain of the translations is different than that of the + current catalog, they are added as a catalog that is only accessible + by the various ``d*gettext`` functions. + + :param translations: the `Translations` instance with the messages to + add + :param merge: whether translations for message domains that have + already been added should be merged with the existing + translations + :return: the `Translations` instance (``self``) so that `merge` calls + can be easily chained + :rtype: `Translations` + """ + domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN) + if merge and domain == self.domain: + return self.merge(translations) + + existing = self._domains.get(domain) + if merge and existing is not None: + existing.merge(translations) + else: + translations.add_fallback(self) + self._domains[domain] = translations + + return self + + def merge(self, translations): + """Merge the given translations into the catalog. + + Message translations in the specified 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, gettext.GNUTranslations): + self._catalog.update(translations._catalog) + if isinstance(translations, Translations): + self.files.extend(translations.files) + + return self + + def dgettext(self, domain, message): + """Like ``gettext()``, but look the message up in the specified + domain. + """ + return self._domains.get(domain, self).gettext(message) + + def ldgettext(self, domain, message): + """Like ``lgettext()``, but look the message up in the specified + domain. + """ + return self._domains.get(domain, self).lgettext(message) + + def dugettext(self, domain, message): + """Like ``ugettext()``, but look the message up in the specified + domain. + """ + return self._domains.get(domain, self).ugettext(message) + + def dngettext(self, domain, singular, plural, num): + """Like ``ngettext()``, but look the message up in the specified + domain. + """ + return self._domains.get(domain, self).ngettext(singular, plural, num) + + def ldngettext(self, domain, singular, plural, num): + """Like ``lngettext()``, but look the message up in the specified + domain. + """ + return self._domains.get(domain, self).lngettext(singular, plural, num) + + def dungettext(self, domain, singular, plural, num): + """Like ``ungettext()`` but look the message up in the specified + domain. + """ + return self._domains.get(domain, self).ungettext(singular, plural, num) diff --git a/justice/external/babel/tests/__init__.py b/justice/external/babel/tests/__init__.py index 6d40716..5105000 100644 --- a/justice/external/babel/tests/__init__.py +++ b/justice/external/babel/tests/__init__.py @@ -1,30 +1,30 @@ -# -*- 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 unittest - -def suite(): - from babel.tests import core, dates, localedata, numbers, support, util - from babel.messages import tests as messages - suite = unittest.TestSuite() - suite.addTest(core.suite()) - suite.addTest(dates.suite()) - suite.addTest(localedata.suite()) - suite.addTest(messages.suite()) - suite.addTest(numbers.suite()) - suite.addTest(support.suite()) - suite.addTest(util.suite()) - return suite - -if __name__ == '__main__': - unittest.main(defaultTest='suite') +# -*- 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 unittest + +def suite(): + from babel.tests import core, dates, localedata, numbers, support, util + from babel.messages import tests as messages + suite = unittest.TestSuite() + suite.addTest(core.suite()) + suite.addTest(dates.suite()) + suite.addTest(localedata.suite()) + suite.addTest(messages.suite()) + suite.addTest(numbers.suite()) + suite.addTest(support.suite()) + suite.addTest(util.suite()) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/justice/external/babel/tests/core.py b/justice/external/babel/tests/core.py index b1b11c9..1abca82 100644 --- a/justice/external/babel/tests/core.py +++ b/justice/external/babel/tests/core.py @@ -1,53 +1,53 @@ -# -*- 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 -import os -import unittest - -from babel import core -from babel.core import default_locale - -class DefaultLocaleTest(unittest.TestCase): - - def setUp(self): - self._old_locale_settings = self._current_locale_settings() - - def tearDown(self): - self._set_locale_settings(self._old_locale_settings) - - def _current_locale_settings(self): - settings = {} - for name in ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG'): - settings[name] = os.environ[name] - return settings - - def _set_locale_settings(self, settings): - for name, value in settings.items(): - os.environ[name] = value - - def test_ignore_invalid_locales_in_lc_ctype(self): - # This is a regression test specifically for a bad LC_CTYPE setting on - # MacOS X 10.6 (#200) - os.environ['LC_CTYPE'] = 'UTF-8' - # must not throw an exception - default_locale('LC_CTYPE') - -def suite(): - suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(core)) - suite.addTest(unittest.makeSuite(DefaultLocaleTest)) - return suite - -if __name__ == '__main__': - unittest.main(defaultTest='suite') +# -*- 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 +import os +import unittest + +from babel import core +from babel.core import default_locale + +class DefaultLocaleTest(unittest.TestCase): + + def setUp(self): + self._old_locale_settings = self._current_locale_settings() + + def tearDown(self): + self._set_locale_settings(self._old_locale_settings) + + def _current_locale_settings(self): + settings = {} + for name in ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG'): + settings[name] = os.environ[name] + return settings + + def _set_locale_settings(self, settings): + for name, value in settings.items(): + os.environ[name] = value + + def test_ignore_invalid_locales_in_lc_ctype(self): + # This is a regression test specifically for a bad LC_CTYPE setting on + # MacOS X 10.6 (#200) + os.environ['LC_CTYPE'] = 'UTF-8' + # must not throw an exception + default_locale('LC_CTYPE') + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(core)) + suite.addTest(unittest.makeSuite(DefaultLocaleTest)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/justice/external/babel/tests/dates.py b/justice/external/babel/tests/dates.py index c36090f..8be06cb 100644 --- a/justice/external/babel/tests/dates.py +++ b/justice/external/babel/tests/dates.py @@ -1,280 +1,280 @@ -# -*- 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/. - -from datetime import date, datetime, time -import doctest -import new -import unittest - -from pytz import timezone - -from babel import dates -from babel.util import FixedOffsetTimezone - - -class DateTimeFormatTestCase(unittest.TestCase): - - def test_quarter_format(self): - d = date(2006, 6, 8) - fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('2', fmt['Q']) - self.assertEqual('2nd quarter', fmt['QQQQ']) - d = date(2006, 12, 31) - fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('Q4', fmt['QQQ']) - - def test_month_context(self): - d = date(2006, 1, 8) - fmt = dates.DateTimeFormat(d, locale='cs_CZ') - self.assertEqual('1', fmt['MMM']) - fmt = dates.DateTimeFormat(d, locale='cs_CZ') - self.assertEqual('1.', fmt['LLL']) - - def test_abbreviated_month_alias(self): - d = date(2006, 3, 8) - fmt = dates.DateTimeFormat(d, locale='de_DE') - self.assertEqual(u'Mär', fmt['LLL']) - - def test_week_of_year_first(self): - d = date(2006, 1, 8) - fmt = dates.DateTimeFormat(d, locale='de_DE') - self.assertEqual('1', fmt['w']) - fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('02', fmt['ww']) - - def test_week_of_year_first_with_year(self): - d = date(2006, 1, 1) - fmt = dates.DateTimeFormat(d, locale='de_DE') - self.assertEqual('52', fmt['w']) - self.assertEqual('2005', fmt['YYYY']) - - def test_week_of_year_last(self): - d = date(2005, 12, 26) - fmt = dates.DateTimeFormat(d, locale='de_DE') - self.assertEqual('52', fmt['w']) - fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('53', fmt['ww']) - - def test_week_of_month_first(self): - d = date(2006, 1, 8) - fmt = dates.DateTimeFormat(d, locale='de_DE') - self.assertEqual('1', fmt['W']) - fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('2', fmt['W']) - - def test_week_of_month_last(self): - d = date(2006, 1, 29) - fmt = dates.DateTimeFormat(d, locale='de_DE') - self.assertEqual('4', fmt['W']) - fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('5', fmt['W']) - - def test_day_of_year(self): - d = date(2007, 4, 1) - fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('91', fmt['D']) - - def test_day_of_year_first(self): - d = date(2007, 1, 1) - fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('001', fmt['DDD']) - - def test_day_of_year_last(self): - d = date(2007, 12, 31) - fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('365', fmt['DDD']) - - def test_day_of_week_in_month(self): - d = date(2007, 4, 15) - fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('3', fmt['F']) - - def test_day_of_week_in_month_first(self): - d = date(2007, 4, 1) - fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('1', fmt['F']) - - def test_day_of_week_in_month_last(self): - d = date(2007, 4, 29) - fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('5', fmt['F']) - - def test_local_day_of_week(self): - d = date(2007, 4, 1) # a sunday - fmt = dates.DateTimeFormat(d, locale='de_DE') - self.assertEqual('7', fmt['e']) # monday is first day of week - fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('01', fmt['ee']) # sunday is first day of week - fmt = dates.DateTimeFormat(d, locale='dv_MV') - self.assertEqual('03', fmt['ee']) # friday is first day of week - - d = date(2007, 4, 2) # a monday - fmt = dates.DateTimeFormat(d, locale='de_DE') - self.assertEqual('1', fmt['e']) # monday is first day of week - fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('02', fmt['ee']) # sunday is first day of week - fmt = dates.DateTimeFormat(d, locale='dv_MV') - self.assertEqual('04', fmt['ee']) # friday is first day of week - - def test_local_day_of_week_standalone(self): - d = date(2007, 4, 1) # a sunday - fmt = dates.DateTimeFormat(d, locale='de_DE') - self.assertEqual('7', fmt['c']) # monday is first day of week - fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('1', fmt['c']) # sunday is first day of week - fmt = dates.DateTimeFormat(d, locale='dv_MV') - self.assertEqual('3', fmt['c']) # friday is first day of week - - d = date(2007, 4, 2) # a monday - fmt = dates.DateTimeFormat(d, locale='de_DE') - self.assertEqual('1', fmt['c']) # monday is first day of week - fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('2', fmt['c']) # sunday is first day of week - fmt = dates.DateTimeFormat(d, locale='dv_MV') - self.assertEqual('4', fmt['c']) # friday is first day of week - - def test_fractional_seconds(self): - t = time(15, 30, 12, 34567) - fmt = dates.DateTimeFormat(t, locale='en_US') - self.assertEqual('3457', fmt['SSSS']) - - def test_fractional_seconds_zero(self): - t = time(15, 30, 0) - fmt = dates.DateTimeFormat(t, locale='en_US') - self.assertEqual('0000', fmt['SSSS']) - - def test_milliseconds_in_day(self): - t = time(15, 30, 12, 345000) - fmt = dates.DateTimeFormat(t, locale='en_US') - self.assertEqual('55812345', fmt['AAAA']) - - def test_milliseconds_in_day_zero(self): - d = time(0, 0, 0) - fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('0000', fmt['AAAA']) - - def test_timezone_rfc822(self): - tz = timezone('Europe/Berlin') - t = time(15, 30, tzinfo=tz) - fmt = dates.DateTimeFormat(t, locale='de_DE') - self.assertEqual('+0100', fmt['Z']) - - def test_timezone_gmt(self): - tz = timezone('Europe/Berlin') - t = time(15, 30, tzinfo=tz) - fmt = dates.DateTimeFormat(t, locale='de_DE') - self.assertEqual('GMT+01:00', fmt['ZZZZ']) - - def test_timezone_no_uncommon(self): - tz = timezone('Europe/Paris') - dt = datetime(2007, 4, 1, 15, 30, tzinfo=tz) - fmt = dates.DateTimeFormat(dt, locale='fr_CA') - self.assertEqual('France', fmt['v']) - - def test_timezone_with_uncommon(self): - tz = timezone('Europe/Paris') - dt = datetime(2007, 4, 1, 15, 30, tzinfo=tz) - fmt = dates.DateTimeFormat(dt, locale='fr_CA') - self.assertEqual('HEC', fmt['V']) - - def test_timezone_location_format(self): - tz = timezone('Europe/Paris') - dt = datetime(2007, 4, 1, 15, 30, tzinfo=tz) - fmt = dates.DateTimeFormat(dt, locale='fr_FR') - self.assertEqual('France', fmt['VVVV']) - - def test_timezone_walltime_short(self): - tz = timezone('Europe/Paris') - t = time(15, 30, tzinfo=tz) - fmt = dates.DateTimeFormat(t, locale='fr_FR') - self.assertEqual('HEC', fmt['v']) - - def test_timezone_walltime_long(self): - tz = timezone('Europe/Paris') - t = time(15, 30, tzinfo=tz) - fmt = dates.DateTimeFormat(t, locale='fr_FR') - self.assertEqual(u'heure d’Europe centrale', fmt['vvvv']) - - def test_hour_formatting(self): - l = 'en_US' - t = time(0, 0, 0) - self.assertEqual(dates.format_time(t, 'h a', locale=l), '12 AM') - self.assertEqual(dates.format_time(t, 'H', locale=l), '0') - self.assertEqual(dates.format_time(t, 'k', locale=l), '24') - self.assertEqual(dates.format_time(t, 'K a', locale=l), '0 AM') - t = time(12, 0, 0) - self.assertEqual(dates.format_time(t, 'h a', locale=l), '12 PM') - self.assertEqual(dates.format_time(t, 'H', locale=l), '12') - self.assertEqual(dates.format_time(t, 'k', locale=l), '12') - self.assertEqual(dates.format_time(t, 'K a', locale=l), '0 PM') - - -class FormatDateTestCase(unittest.TestCase): - - def test_with_time_fields_in_pattern(self): - self.assertRaises(AttributeError, dates.format_date, date(2007, 04, 01), - "yyyy-MM-dd HH:mm", locale='en_US') - - def test_with_time_fields_in_pattern_and_datetime_param(self): - self.assertRaises(AttributeError, dates.format_date, - datetime(2007, 04, 01, 15, 30), - "yyyy-MM-dd HH:mm", locale='en_US') - - -class FormatTimeTestCase(unittest.TestCase): - - def test_with_naive_datetime_and_tzinfo(self): - string = dates.format_time(datetime(2007, 4, 1, 15, 30), - 'long', tzinfo=timezone('US/Eastern'), - locale='en') - self.assertEqual('11:30:00 AM EDT', string) - - def test_with_date_fields_in_pattern(self): - self.assertRaises(AttributeError, dates.format_time, date(2007, 04, 01), - "yyyy-MM-dd HH:mm", locale='en_US') - - def test_with_date_fields_in_pattern_and_datetime_param(self): - self.assertRaises(AttributeError, dates.format_time, - datetime(2007, 04, 01, 15, 30), - "yyyy-MM-dd HH:mm", locale='en_US') - - -class TimeZoneAdjustTestCase(unittest.TestCase): - def _utc(self): - UTC = FixedOffsetTimezone(0, 'UTC') - def fake_localize(self, dt, is_dst=False): - raise NotImplementedError() - UTC.localize = new.instancemethod(fake_localize, UTC, UTC.__class__) - # This is important to trigger the actual bug (#257) - self.assertEqual(False, hasattr(UTC, 'normalize')) - return UTC - - def test_can_format_time_with_non_pytz_timezone(self): - # regression test for #257 - utc = self._utc() - t = datetime(2007, 4, 1, 15, 30, tzinfo=utc) - formatted_time = dates.format_time(t, 'long', tzinfo=utc, locale='en') - self.assertEqual('3:30:00 PM +0000', formatted_time) - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(dates)) - suite.addTest(unittest.makeSuite(DateTimeFormatTestCase)) - suite.addTest(unittest.makeSuite(FormatDateTestCase)) - suite.addTest(unittest.makeSuite(FormatTimeTestCase)) - suite.addTest(unittest.makeSuite(TimeZoneAdjustTestCase)) - return suite - -if __name__ == '__main__': - unittest.main(defaultTest='suite') +# -*- 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/. + +from datetime import date, datetime, time +import doctest +import new +import unittest + +from pytz import timezone + +from babel import dates +from babel.util import FixedOffsetTimezone + + +class DateTimeFormatTestCase(unittest.TestCase): + + def test_quarter_format(self): + d = date(2006, 6, 8) + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('2', fmt['Q']) + self.assertEqual('2nd quarter', fmt['QQQQ']) + d = date(2006, 12, 31) + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('Q4', fmt['QQQ']) + + def test_month_context(self): + d = date(2006, 1, 8) + fmt = dates.DateTimeFormat(d, locale='cs_CZ') + self.assertEqual('1', fmt['MMM']) + fmt = dates.DateTimeFormat(d, locale='cs_CZ') + self.assertEqual('1.', fmt['LLL']) + + def test_abbreviated_month_alias(self): + d = date(2006, 3, 8) + fmt = dates.DateTimeFormat(d, locale='de_DE') + self.assertEqual(u'Mär', fmt['LLL']) + + def test_week_of_year_first(self): + d = date(2006, 1, 8) + fmt = dates.DateTimeFormat(d, locale='de_DE') + self.assertEqual('1', fmt['w']) + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('02', fmt['ww']) + + def test_week_of_year_first_with_year(self): + d = date(2006, 1, 1) + fmt = dates.DateTimeFormat(d, locale='de_DE') + self.assertEqual('52', fmt['w']) + self.assertEqual('2005', fmt['YYYY']) + + def test_week_of_year_last(self): + d = date(2005, 12, 26) + fmt = dates.DateTimeFormat(d, locale='de_DE') + self.assertEqual('52', fmt['w']) + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('53', fmt['ww']) + + def test_week_of_month_first(self): + d = date(2006, 1, 8) + fmt = dates.DateTimeFormat(d, locale='de_DE') + self.assertEqual('1', fmt['W']) + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('2', fmt['W']) + + def test_week_of_month_last(self): + d = date(2006, 1, 29) + fmt = dates.DateTimeFormat(d, locale='de_DE') + self.assertEqual('4', fmt['W']) + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('5', fmt['W']) + + def test_day_of_year(self): + d = date(2007, 4, 1) + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('91', fmt['D']) + + def test_day_of_year_first(self): + d = date(2007, 1, 1) + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('001', fmt['DDD']) + + def test_day_of_year_last(self): + d = date(2007, 12, 31) + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('365', fmt['DDD']) + + def test_day_of_week_in_month(self): + d = date(2007, 4, 15) + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('3', fmt['F']) + + def test_day_of_week_in_month_first(self): + d = date(2007, 4, 1) + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('1', fmt['F']) + + def test_day_of_week_in_month_last(self): + d = date(2007, 4, 29) + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('5', fmt['F']) + + def test_local_day_of_week(self): + d = date(2007, 4, 1) # a sunday + fmt = dates.DateTimeFormat(d, locale='de_DE') + self.assertEqual('7', fmt['e']) # monday is first day of week + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('01', fmt['ee']) # sunday is first day of week + fmt = dates.DateTimeFormat(d, locale='dv_MV') + self.assertEqual('03', fmt['ee']) # friday is first day of week + + d = date(2007, 4, 2) # a monday + fmt = dates.DateTimeFormat(d, locale='de_DE') + self.assertEqual('1', fmt['e']) # monday is first day of week + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('02', fmt['ee']) # sunday is first day of week + fmt = dates.DateTimeFormat(d, locale='dv_MV') + self.assertEqual('04', fmt['ee']) # friday is first day of week + + def test_local_day_of_week_standalone(self): + d = date(2007, 4, 1) # a sunday + fmt = dates.DateTimeFormat(d, locale='de_DE') + self.assertEqual('7', fmt['c']) # monday is first day of week + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('1', fmt['c']) # sunday is first day of week + fmt = dates.DateTimeFormat(d, locale='dv_MV') + self.assertEqual('3', fmt['c']) # friday is first day of week + + d = date(2007, 4, 2) # a monday + fmt = dates.DateTimeFormat(d, locale='de_DE') + self.assertEqual('1', fmt['c']) # monday is first day of week + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('2', fmt['c']) # sunday is first day of week + fmt = dates.DateTimeFormat(d, locale='dv_MV') + self.assertEqual('4', fmt['c']) # friday is first day of week + + def test_fractional_seconds(self): + t = time(15, 30, 12, 34567) + fmt = dates.DateTimeFormat(t, locale='en_US') + self.assertEqual('3457', fmt['SSSS']) + + def test_fractional_seconds_zero(self): + t = time(15, 30, 0) + fmt = dates.DateTimeFormat(t, locale='en_US') + self.assertEqual('0000', fmt['SSSS']) + + def test_milliseconds_in_day(self): + t = time(15, 30, 12, 345000) + fmt = dates.DateTimeFormat(t, locale='en_US') + self.assertEqual('55812345', fmt['AAAA']) + + def test_milliseconds_in_day_zero(self): + d = time(0, 0, 0) + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('0000', fmt['AAAA']) + + def test_timezone_rfc822(self): + tz = timezone('Europe/Berlin') + t = time(15, 30, tzinfo=tz) + fmt = dates.DateTimeFormat(t, locale='de_DE') + self.assertEqual('+0100', fmt['Z']) + + def test_timezone_gmt(self): + tz = timezone('Europe/Berlin') + t = time(15, 30, tzinfo=tz) + fmt = dates.DateTimeFormat(t, locale='de_DE') + self.assertEqual('GMT+01:00', fmt['ZZZZ']) + + def test_timezone_no_uncommon(self): + tz = timezone('Europe/Paris') + dt = datetime(2007, 4, 1, 15, 30, tzinfo=tz) + fmt = dates.DateTimeFormat(dt, locale='fr_CA') + self.assertEqual('France', fmt['v']) + + def test_timezone_with_uncommon(self): + tz = timezone('Europe/Paris') + dt = datetime(2007, 4, 1, 15, 30, tzinfo=tz) + fmt = dates.DateTimeFormat(dt, locale='fr_CA') + self.assertEqual('HEC', fmt['V']) + + def test_timezone_location_format(self): + tz = timezone('Europe/Paris') + dt = datetime(2007, 4, 1, 15, 30, tzinfo=tz) + fmt = dates.DateTimeFormat(dt, locale='fr_FR') + self.assertEqual('France', fmt['VVVV']) + + def test_timezone_walltime_short(self): + tz = timezone('Europe/Paris') + t = time(15, 30, tzinfo=tz) + fmt = dates.DateTimeFormat(t, locale='fr_FR') + self.assertEqual('HEC', fmt['v']) + + def test_timezone_walltime_long(self): + tz = timezone('Europe/Paris') + t = time(15, 30, tzinfo=tz) + fmt = dates.DateTimeFormat(t, locale='fr_FR') + self.assertEqual(u'heure d’Europe centrale', fmt['vvvv']) + + def test_hour_formatting(self): + l = 'en_US' + t = time(0, 0, 0) + self.assertEqual(dates.format_time(t, 'h a', locale=l), '12 AM') + self.assertEqual(dates.format_time(t, 'H', locale=l), '0') + self.assertEqual(dates.format_time(t, 'k', locale=l), '24') + self.assertEqual(dates.format_time(t, 'K a', locale=l), '0 AM') + t = time(12, 0, 0) + self.assertEqual(dates.format_time(t, 'h a', locale=l), '12 PM') + self.assertEqual(dates.format_time(t, 'H', locale=l), '12') + self.assertEqual(dates.format_time(t, 'k', locale=l), '12') + self.assertEqual(dates.format_time(t, 'K a', locale=l), '0 PM') + + +class FormatDateTestCase(unittest.TestCase): + + def test_with_time_fields_in_pattern(self): + self.assertRaises(AttributeError, dates.format_date, date(2007, 04, 01), + "yyyy-MM-dd HH:mm", locale='en_US') + + def test_with_time_fields_in_pattern_and_datetime_param(self): + self.assertRaises(AttributeError, dates.format_date, + datetime(2007, 04, 01, 15, 30), + "yyyy-MM-dd HH:mm", locale='en_US') + + +class FormatTimeTestCase(unittest.TestCase): + + def test_with_naive_datetime_and_tzinfo(self): + string = dates.format_time(datetime(2007, 4, 1, 15, 30), + 'long', tzinfo=timezone('US/Eastern'), + locale='en') + self.assertEqual('11:30:00 AM EDT', string) + + def test_with_date_fields_in_pattern(self): + self.assertRaises(AttributeError, dates.format_time, date(2007, 04, 01), + "yyyy-MM-dd HH:mm", locale='en_US') + + def test_with_date_fields_in_pattern_and_datetime_param(self): + self.assertRaises(AttributeError, dates.format_time, + datetime(2007, 04, 01, 15, 30), + "yyyy-MM-dd HH:mm", locale='en_US') + + +class TimeZoneAdjustTestCase(unittest.TestCase): + def _utc(self): + UTC = FixedOffsetTimezone(0, 'UTC') + def fake_localize(self, dt, is_dst=False): + raise NotImplementedError() + UTC.localize = new.instancemethod(fake_localize, UTC, UTC.__class__) + # This is important to trigger the actual bug (#257) + self.assertEqual(False, hasattr(UTC, 'normalize')) + return UTC + + def test_can_format_time_with_non_pytz_timezone(self): + # regression test for #257 + utc = self._utc() + t = datetime(2007, 4, 1, 15, 30, tzinfo=utc) + formatted_time = dates.format_time(t, 'long', tzinfo=utc, locale='en') + self.assertEqual('3:30:00 PM +0000', formatted_time) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(dates)) + suite.addTest(unittest.makeSuite(DateTimeFormatTestCase)) + suite.addTest(unittest.makeSuite(FormatDateTestCase)) + suite.addTest(unittest.makeSuite(FormatTimeTestCase)) + suite.addTest(unittest.makeSuite(TimeZoneAdjustTestCase)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/justice/external/babel/tests/localedata.py b/justice/external/babel/tests/localedata.py index ea0a483..bf5c183 100644 --- a/justice/external/babel/tests/localedata.py +++ b/justice/external/babel/tests/localedata.py @@ -1,73 +1,73 @@ -# -*- 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 -import unittest - -from babel import localedata - - -class MergeResolveTestCase(unittest.TestCase): - - def test_merge_items(self): - d = {1: 'foo', 3: 'baz'} - localedata.merge(d, {1: 'Foo', 2: 'Bar'}) - self.assertEqual({1: 'Foo', 2: 'Bar', 3: 'baz'}, d) - - def test_merge_nested_dict(self): - d1 = {'x': {'a': 1, 'b': 2, 'c': 3}} - d2 = {'x': {'a': 1, 'b': 12, 'd': 14}} - localedata.merge(d1, d2) - self.assertEqual({ - 'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14} - }, d1) - - def test_merge_nested_dict_no_overlap(self): - d1 = {'x': {'a': 1, 'b': 2}} - d2 = {'y': {'a': 11, 'b': 12}} - localedata.merge(d1, d2) - self.assertEqual({ - 'x': {'a': 1, 'b': 2}, - 'y': {'a': 11, 'b': 12} - }, d1) - - def test_merge_with_alias_and_resolve(self): - alias = localedata.Alias('x') - d1 = { - 'x': {'a': 1, 'b': 2, 'c': 3}, - 'y': alias - } - d2 = { - 'x': {'a': 1, 'b': 12, 'd': 14}, - 'y': {'b': 22, 'e': 25} - } - localedata.merge(d1, d2) - self.assertEqual({ - 'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14}, - 'y': (alias, {'b': 22, 'e': 25}) - }, d1) - d = localedata.LocaleDataDict(d1) - self.assertEqual({ - 'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14}, - 'y': {'a': 1, 'b': 22, 'c': 3, 'd': 14, 'e': 25} - }, dict(d.items())) - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(localedata)) - suite.addTest(unittest.makeSuite(MergeResolveTestCase)) - return suite - -if __name__ == '__main__': - unittest.main(defaultTest='suite') +# -*- 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 +import unittest + +from babel import localedata + + +class MergeResolveTestCase(unittest.TestCase): + + def test_merge_items(self): + d = {1: 'foo', 3: 'baz'} + localedata.merge(d, {1: 'Foo', 2: 'Bar'}) + self.assertEqual({1: 'Foo', 2: 'Bar', 3: 'baz'}, d) + + def test_merge_nested_dict(self): + d1 = {'x': {'a': 1, 'b': 2, 'c': 3}} + d2 = {'x': {'a': 1, 'b': 12, 'd': 14}} + localedata.merge(d1, d2) + self.assertEqual({ + 'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14} + }, d1) + + def test_merge_nested_dict_no_overlap(self): + d1 = {'x': {'a': 1, 'b': 2}} + d2 = {'y': {'a': 11, 'b': 12}} + localedata.merge(d1, d2) + self.assertEqual({ + 'x': {'a': 1, 'b': 2}, + 'y': {'a': 11, 'b': 12} + }, d1) + + def test_merge_with_alias_and_resolve(self): + alias = localedata.Alias('x') + d1 = { + 'x': {'a': 1, 'b': 2, 'c': 3}, + 'y': alias + } + d2 = { + 'x': {'a': 1, 'b': 12, 'd': 14}, + 'y': {'b': 22, 'e': 25} + } + localedata.merge(d1, d2) + self.assertEqual({ + 'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14}, + 'y': (alias, {'b': 22, 'e': 25}) + }, d1) + d = localedata.LocaleDataDict(d1) + self.assertEqual({ + 'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14}, + 'y': {'a': 1, 'b': 22, 'c': 3, 'd': 14, 'e': 25} + }, dict(d.items())) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(localedata)) + suite.addTest(unittest.makeSuite(MergeResolveTestCase)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/justice/external/babel/tests/numbers.py b/justice/external/babel/tests/numbers.py index 5e43e96..3e634e4 100644 --- a/justice/external/babel/tests/numbers.py +++ b/justice/external/babel/tests/numbers.py @@ -1,153 +1,153 @@ -# -*- 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/. - -try: - from decimal import Decimal - have_decimal = True -except ImportError: - have_decimal = False - -import doctest -import unittest - -from babel import numbers - - -class FormatDecimalTestCase(unittest.TestCase): - - def test_patterns(self): - self.assertEqual(numbers.format_decimal(12345, '##0', - locale='en_US'), '12345') - self.assertEqual(numbers.format_decimal(6.5, '0.00', locale='sv'), - '6,50') - self.assertEqual(numbers.format_decimal(10.0**20, - '#.00', locale='en_US'), - '100000000000000000000.00') - - def test_subpatterns(self): - self.assertEqual(numbers.format_decimal(-12345, '#,##0.##;-#', - locale='en_US'), '-12,345') - self.assertEqual(numbers.format_decimal(-12345, '#,##0.##;(#)', - locale='en_US'), '(12,345)') - - def test_default_rounding(self): - """ - Testing Round-Half-Even (Banker's rounding) - - A '5' is rounded to the closest 'even' number - """ - self.assertEqual(numbers.format_decimal(5.5, '0', locale='sv'), '6') - self.assertEqual(numbers.format_decimal(6.5, '0', locale='sv'), '6') - self.assertEqual(numbers.format_decimal(6.5, '0', locale='sv'), '6') - self.assertEqual(numbers.format_decimal(1.2325, locale='sv'), '1,232') - self.assertEqual(numbers.format_decimal(1.2335, locale='sv'), '1,234') - - def test_significant_digits(self): - """Test significant digits patterns""" - self.assertEqual(numbers.format_decimal(123004, '@@',locale='en_US'), - '120000') - self.assertEqual(numbers.format_decimal(1.12, '@', locale='sv'), '1') - self.assertEqual(numbers.format_decimal(1.1, '@@', locale='sv'), '1,1') - self.assertEqual(numbers.format_decimal(1.1, '@@@@@##', locale='sv'), - '1,1000') - self.assertEqual(numbers.format_decimal(0.0001, '@@@', locale='sv'), - '0,000100') - self.assertEqual(numbers.format_decimal(0.0001234, '@@@', locale='sv'), - '0,000123') - self.assertEqual(numbers.format_decimal(0.0001234, '@@@#',locale='sv'), - '0,0001234') - self.assertEqual(numbers.format_decimal(0.0001234, '@@@#',locale='sv'), - '0,0001234') - self.assertEqual(numbers.format_decimal(0.12345, '@@@',locale='sv'), - '0,123') - self.assertEqual(numbers.format_decimal(3.14159, '@@##',locale='sv'), - '3,142') - self.assertEqual(numbers.format_decimal(1.23004, '@@##',locale='sv'), - '1,23') - self.assertEqual(numbers.format_decimal(1230.04, '@@,@@',locale='en_US'), - '12,30') - self.assertEqual(numbers.format_decimal(123.41, '@@##',locale='en_US'), - '123.4') - self.assertEqual(numbers.format_decimal(1, '@@',locale='en_US'), - '1.0') - self.assertEqual(numbers.format_decimal(0, '@',locale='en_US'), - '0') - self.assertEqual(numbers.format_decimal(0.1, '@',locale='en_US'), - '0.1') - self.assertEqual(numbers.format_decimal(0.1, '@#',locale='en_US'), - '0.1') - self.assertEqual(numbers.format_decimal(0.1, '@@', locale='en_US'), - '0.10') - - if have_decimal: - def test_decimals(self): - """Test significant digits patterns""" - self.assertEqual(numbers.format_decimal(Decimal('1.2345'), - '#.00', locale='en_US'), - '1.23') - self.assertEqual(numbers.format_decimal(Decimal('1.2345000'), - '#.00', locale='en_US'), - '1.23') - self.assertEqual(numbers.format_decimal(Decimal('1.2345000'), - '@@', locale='en_US'), - '1.2') - self.assertEqual(numbers.format_decimal(Decimal('12345678901234567890.12345'), - '#.00', locale='en_US'), - '12345678901234567890.12') - - def test_scientific_notation(self): - fmt = numbers.format_scientific(0.1, '#E0', locale='en_US') - self.assertEqual(fmt, '1E-1') - fmt = numbers.format_scientific(0.01, '#E0', locale='en_US') - self.assertEqual(fmt, '1E-2') - fmt = numbers.format_scientific(10, '#E0', locale='en_US') - self.assertEqual(fmt, '1E1') - fmt = numbers.format_scientific(1234, '0.###E0', locale='en_US') - self.assertEqual(fmt, '1.234E3') - fmt = numbers.format_scientific(1234, '0.#E0', locale='en_US') - self.assertEqual(fmt, '1.2E3') - # Exponent grouping - fmt = numbers.format_scientific(12345, '##0.####E0', locale='en_US') - self.assertEqual(fmt, '12.345E3') - # Minimum number of int digits - fmt = numbers.format_scientific(12345, '00.###E0', locale='en_US') - self.assertEqual(fmt, '12.345E3') - fmt = numbers.format_scientific(-12345.6, '00.###E0', locale='en_US') - self.assertEqual(fmt, '-12.346E3') - fmt = numbers.format_scientific(-0.01234, '00.###E0', locale='en_US') - self.assertEqual(fmt, '-12.34E-3') - # Custom pattern suffic - fmt = numbers.format_scientific(123.45, '#.##E0 m/s', locale='en_US') - self.assertEqual(fmt, '1.23E2 m/s') - # Exponent patterns - fmt = numbers.format_scientific(123.45, '#.##E00 m/s', locale='en_US') - self.assertEqual(fmt, '1.23E02 m/s') - fmt = numbers.format_scientific(0.012345, '#.##E00 m/s', locale='en_US') - self.assertEqual(fmt, '1.23E-02 m/s') - if have_decimal: - fmt = numbers.format_scientific(Decimal('12345'), '#.##E+00 m/s', - locale='en_US') - self.assertEqual(fmt, '1.23E+04 m/s') - # 0 (see ticket #99) - fmt = numbers.format_scientific(0, '#E0', locale='en_US') - self.assertEqual(fmt, '0E0') - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(numbers)) - suite.addTest(unittest.makeSuite(FormatDecimalTestCase)) - return suite - -if __name__ == '__main__': - unittest.main(defaultTest='suite') +# -*- 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/. + +try: + from decimal import Decimal + have_decimal = True +except ImportError: + have_decimal = False + +import doctest +import unittest + +from babel import numbers + + +class FormatDecimalTestCase(unittest.TestCase): + + def test_patterns(self): + self.assertEqual(numbers.format_decimal(12345, '##0', + locale='en_US'), '12345') + self.assertEqual(numbers.format_decimal(6.5, '0.00', locale='sv'), + '6,50') + self.assertEqual(numbers.format_decimal(10.0**20, + '#.00', locale='en_US'), + '100000000000000000000.00') + + def test_subpatterns(self): + self.assertEqual(numbers.format_decimal(-12345, '#,##0.##;-#', + locale='en_US'), '-12,345') + self.assertEqual(numbers.format_decimal(-12345, '#,##0.##;(#)', + locale='en_US'), '(12,345)') + + def test_default_rounding(self): + """ + Testing Round-Half-Even (Banker's rounding) + + A '5' is rounded to the closest 'even' number + """ + self.assertEqual(numbers.format_decimal(5.5, '0', locale='sv'), '6') + self.assertEqual(numbers.format_decimal(6.5, '0', locale='sv'), '6') + self.assertEqual(numbers.format_decimal(6.5, '0', locale='sv'), '6') + self.assertEqual(numbers.format_decimal(1.2325, locale='sv'), '1,232') + self.assertEqual(numbers.format_decimal(1.2335, locale='sv'), '1,234') + + def test_significant_digits(self): + """Test significant digits patterns""" + self.assertEqual(numbers.format_decimal(123004, '@@',locale='en_US'), + '120000') + self.assertEqual(numbers.format_decimal(1.12, '@', locale='sv'), '1') + self.assertEqual(numbers.format_decimal(1.1, '@@', locale='sv'), '1,1') + self.assertEqual(numbers.format_decimal(1.1, '@@@@@##', locale='sv'), + '1,1000') + self.assertEqual(numbers.format_decimal(0.0001, '@@@', locale='sv'), + '0,000100') + self.assertEqual(numbers.format_decimal(0.0001234, '@@@', locale='sv'), + '0,000123') + self.assertEqual(numbers.format_decimal(0.0001234, '@@@#',locale='sv'), + '0,0001234') + self.assertEqual(numbers.format_decimal(0.0001234, '@@@#',locale='sv'), + '0,0001234') + self.assertEqual(numbers.format_decimal(0.12345, '@@@',locale='sv'), + '0,123') + self.assertEqual(numbers.format_decimal(3.14159, '@@##',locale='sv'), + '3,142') + self.assertEqual(numbers.format_decimal(1.23004, '@@##',locale='sv'), + '1,23') + self.assertEqual(numbers.format_decimal(1230.04, '@@,@@',locale='en_US'), + '12,30') + self.assertEqual(numbers.format_decimal(123.41, '@@##',locale='en_US'), + '123.4') + self.assertEqual(numbers.format_decimal(1, '@@',locale='en_US'), + '1.0') + self.assertEqual(numbers.format_decimal(0, '@',locale='en_US'), + '0') + self.assertEqual(numbers.format_decimal(0.1, '@',locale='en_US'), + '0.1') + self.assertEqual(numbers.format_decimal(0.1, '@#',locale='en_US'), + '0.1') + self.assertEqual(numbers.format_decimal(0.1, '@@', locale='en_US'), + '0.10') + + if have_decimal: + def test_decimals(self): + """Test significant digits patterns""" + self.assertEqual(numbers.format_decimal(Decimal('1.2345'), + '#.00', locale='en_US'), + '1.23') + self.assertEqual(numbers.format_decimal(Decimal('1.2345000'), + '#.00', locale='en_US'), + '1.23') + self.assertEqual(numbers.format_decimal(Decimal('1.2345000'), + '@@', locale='en_US'), + '1.2') + self.assertEqual(numbers.format_decimal(Decimal('12345678901234567890.12345'), + '#.00', locale='en_US'), + '12345678901234567890.12') + + def test_scientific_notation(self): + fmt = numbers.format_scientific(0.1, '#E0', locale='en_US') + self.assertEqual(fmt, '1E-1') + fmt = numbers.format_scientific(0.01, '#E0', locale='en_US') + self.assertEqual(fmt, '1E-2') + fmt = numbers.format_scientific(10, '#E0', locale='en_US') + self.assertEqual(fmt, '1E1') + fmt = numbers.format_scientific(1234, '0.###E0', locale='en_US') + self.assertEqual(fmt, '1.234E3') + fmt = numbers.format_scientific(1234, '0.#E0', locale='en_US') + self.assertEqual(fmt, '1.2E3') + # Exponent grouping + fmt = numbers.format_scientific(12345, '##0.####E0', locale='en_US') + self.assertEqual(fmt, '12.345E3') + # Minimum number of int digits + fmt = numbers.format_scientific(12345, '00.###E0', locale='en_US') + self.assertEqual(fmt, '12.345E3') + fmt = numbers.format_scientific(-12345.6, '00.###E0', locale='en_US') + self.assertEqual(fmt, '-12.346E3') + fmt = numbers.format_scientific(-0.01234, '00.###E0', locale='en_US') + self.assertEqual(fmt, '-12.34E-3') + # Custom pattern suffic + fmt = numbers.format_scientific(123.45, '#.##E0 m/s', locale='en_US') + self.assertEqual(fmt, '1.23E2 m/s') + # Exponent patterns + fmt = numbers.format_scientific(123.45, '#.##E00 m/s', locale='en_US') + self.assertEqual(fmt, '1.23E02 m/s') + fmt = numbers.format_scientific(0.012345, '#.##E00 m/s', locale='en_US') + self.assertEqual(fmt, '1.23E-02 m/s') + if have_decimal: + fmt = numbers.format_scientific(Decimal('12345'), '#.##E+00 m/s', + locale='en_US') + self.assertEqual(fmt, '1.23E+04 m/s') + # 0 (see ticket #99) + fmt = numbers.format_scientific(0, '#E0', locale='en_US') + self.assertEqual(fmt, '0E0') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(numbers)) + suite.addTest(unittest.makeSuite(FormatDecimalTestCase)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/justice/external/babel/tests/support.py b/justice/external/babel/tests/support.py index 215e8dd..83586f3 100644 --- a/justice/external/babel/tests/support.py +++ b/justice/external/babel/tests/support.py @@ -1,25 +1,25 @@ -# -*- 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 -import unittest - -from babel import support - -def suite(): - suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(support)) - return suite - -if __name__ == '__main__': - unittest.main(defaultTest='suite') +# -*- 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 +import unittest + +from babel import support + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(support)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/justice/external/babel/tests/util.py b/justice/external/babel/tests/util.py index f393b34..280a46f 100644 --- a/justice/external/babel/tests/util.py +++ b/justice/external/babel/tests/util.py @@ -1,25 +1,25 @@ -# -*- 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 -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') +# -*- 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 +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/justice/external/babel/util.py b/justice/external/babel/util.py index e3a611d..3dacfa4 100644 --- a/justice/external/babel/util.py +++ b/justice/external/babel/util.py @@ -1,370 +1,370 @@ -# -*- 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 codecs -from datetime import timedelta, tzinfo -import os -import re -try: - set = set -except NameError: - from sets import Set as set -import textwrap -import time -from itertools import izip, imap -missing = object() - -__all__ = ['distinct', 'pathmatch', 'relpath', 'wraptext', 'odict', 'UTC', - 'LOCALTZ'] -__docformat__ = 'restructuredtext en' - - -def distinct(iterable): - """Yield all items in an iterable collection that are distinct. - - Unlike when using sets for a similar effect, the original ordering of the - items in the collection is preserved by this function. - - >>> print list(distinct([1, 2, 1, 3, 4, 4])) - [1, 2, 3, 4] - >>> print list(distinct('foobar')) - ['f', 'o', 'b', 'a', 'r'] - - :param iterable: the iterable collection providing the data - :return: the distinct items in the collection - :rtype: ``iterator`` - """ - seen = set() - for item in iter(iterable): - if item not in seen: - yield item - seen.add(item) - -# Regexp to match python magic encoding line -PYTHON_MAGIC_COMMENT_re = re.compile( - r'[ \t\f]* \# .* coding[=:][ \t]*([-\w.]+)', re.VERBOSE) -def parse_encoding(fp): - """Deduce the encoding of a source file from magic comment. - - It does this in the same way as the `Python interpreter`__ - - .. __: http://docs.python.org/ref/encodings.html - - The ``fp`` argument should be a seekable file object. - - (From Jeff Dairiki) - """ - pos = fp.tell() - fp.seek(0) - try: - line1 = fp.readline() - has_bom = line1.startswith(codecs.BOM_UTF8) - if has_bom: - line1 = line1[len(codecs.BOM_UTF8):] - - m = PYTHON_MAGIC_COMMENT_re.match(line1) - if not m: - try: - import parser - parser.suite(line1) - except (ImportError, SyntaxError): - # Either it's a real syntax error, in which case the source is - # not valid python source, or line2 is a continuation of line1, - # in which case we don't want to scan line2 for a magic - # comment. - pass - else: - line2 = fp.readline() - m = PYTHON_MAGIC_COMMENT_re.match(line2) - - if has_bom: - if m: - raise SyntaxError( - "python refuses to compile code with both a UTF8 " - "byte-order-mark and a magic encoding comment") - return 'utf_8' - elif m: - return m.group(1) - else: - return None - finally: - fp.seek(pos) - -def pathmatch(pattern, filename): - """Extended pathname pattern matching. - - This function is similar to what is provided by the ``fnmatch`` module in - the Python standard library, but: - - * can match complete (relative or absolute) path names, and not just file - names, and - * also supports a convenience pattern ("**") to match files at any - directory level. - - Examples: - - >>> pathmatch('**.py', 'bar.py') - True - >>> pathmatch('**.py', 'foo/bar/baz.py') - True - >>> pathmatch('**.py', 'templates/index.html') - False - - >>> pathmatch('**/templates/*.html', 'templates/index.html') - True - >>> pathmatch('**/templates/*.html', 'templates/foo/bar.html') - False - - :param pattern: the glob pattern - :param filename: the path name of the file to match against - :return: `True` if the path name matches the pattern, `False` otherwise - :rtype: `bool` - """ - symbols = { - '?': '[^/]', - '?/': '[^/]/', - '*': '[^/]+', - '*/': '[^/]+/', - '**/': '(?:.+/)*?', - '**': '(?:.+/)*?[^/]+', - } - buf = [] - for idx, part in enumerate(re.split('([?*]+/?)', pattern)): - if idx % 2: - buf.append(symbols[part]) - elif part: - buf.append(re.escape(part)) - match = re.match(''.join(buf) + '$', filename.replace(os.sep, '/')) - return match is not None - - -class TextWrapper(textwrap.TextWrapper): - wordsep_re = re.compile( - r'(\s+|' # any whitespace - r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))' # em-dash - ) - - -def wraptext(text, width=70, initial_indent='', subsequent_indent=''): - """Simple wrapper around the ``textwrap.wrap`` function in the standard - library. This version does not wrap lines on hyphens in words. - - :param text: the text to wrap - :param width: the maximum line width - :param initial_indent: string that will be prepended to the first line of - wrapped output - :param subsequent_indent: string that will be prepended to all lines save - the first of wrapped output - :return: a list of lines - :rtype: `list` - """ - wrapper = TextWrapper(width=width, initial_indent=initial_indent, - subsequent_indent=subsequent_indent, - break_long_words=False) - return wrapper.wrap(text) - - -class odict(dict): - """Ordered dict implementation. - - :see: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/107747 - """ - def __init__(self, data=None): - dict.__init__(self, data or {}) - self._keys = dict.keys(self) - - def __delitem__(self, key): - dict.__delitem__(self, key) - self._keys.remove(key) - - def __setitem__(self, key, item): - dict.__setitem__(self, key, item) - if key not in self._keys: - self._keys.append(key) - - def __iter__(self): - return iter(self._keys) - iterkeys = __iter__ - - def clear(self): - dict.clear(self) - self._keys = [] - - def copy(self): - d = odict() - d.update(self) - return d - - def items(self): - return zip(self._keys, self.values()) - - def iteritems(self): - return izip(self._keys, self.itervalues()) - - def keys(self): - return self._keys[:] - - def pop(self, key, default=missing): - if default is missing: - return dict.pop(self, key) - elif key not in self: - return default - self._keys.remove(key) - return dict.pop(self, key, default) - - def popitem(self, key): - self._keys.remove(key) - return dict.popitem(key) - - def setdefault(self, key, failobj = None): - dict.setdefault(self, key, failobj) - if key not in self._keys: - self._keys.append(key) - - def update(self, dict): - for (key, val) in dict.items(): - self[key] = val - - def values(self): - return map(self.get, self._keys) - - def itervalues(self): - return imap(self.get, self._keys) - - -try: - relpath = os.path.relpath -except AttributeError: - def relpath(path, start='.'): - """Compute the relative path to one path from another. - - >>> relpath('foo/bar.txt', '').replace(os.sep, '/') - 'foo/bar.txt' - >>> relpath('foo/bar.txt', 'foo').replace(os.sep, '/') - 'bar.txt' - >>> relpath('foo/bar.txt', 'baz').replace(os.sep, '/') - '../foo/bar.txt' - - :return: the relative path - :rtype: `basestring` - """ - 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) - -try: - from operator import attrgetter, itemgetter -except ImportError: - def itemgetter(name): - def _getitem(obj): - return obj[name] - return _getitem - -try: - ''.rsplit - def rsplit(a_string, sep=None, maxsplit=None): - return a_string.rsplit(sep, maxsplit) -except AttributeError: - def rsplit(a_string, sep=None, maxsplit=None): - parts = a_string.split(sep) - if maxsplit is None or len(parts) <= maxsplit: - return parts - maxsplit_index = len(parts) - maxsplit - non_splitted_part = sep.join(parts[:maxsplit_index]) - splitted = parts[maxsplit_index:] - return [non_splitted_part] + splitted - -ZERO = timedelta(0) - - -class FixedOffsetTimezone(tzinfo): - """Fixed offset in minutes east from UTC.""" - - def __init__(self, offset, name=None): - self._offset = timedelta(minutes=offset) - if name is None: - name = 'Etc/GMT+%d' % offset - self.zone = name - - def __str__(self): - return self.zone - - def __repr__(self): - return '' % (self.zone, self._offset) - - def utcoffset(self, dt): - return self._offset - - def tzname(self, dt): - return self.zone - - def dst(self, dt): - return ZERO - - -try: - from pytz import UTC -except ImportError: - UTC = FixedOffsetTimezone(0, 'UTC') - """`tzinfo` object for UTC (Universal Time). - - :type: `tzinfo` - """ - -STDOFFSET = timedelta(seconds = -time.timezone) -if time.daylight: - DSTOFFSET = timedelta(seconds = -time.altzone) -else: - DSTOFFSET = STDOFFSET - -DSTDIFF = DSTOFFSET - STDOFFSET - - -class LocalTimezone(tzinfo): - - def utcoffset(self, dt): - if self._isdst(dt): - return DSTOFFSET - else: - return STDOFFSET - - def dst(self, dt): - if self._isdst(dt): - return DSTDIFF - else: - return ZERO - - def tzname(self, dt): - return time.tzname[self._isdst(dt)] - - def _isdst(self, dt): - tt = (dt.year, dt.month, dt.day, - dt.hour, dt.minute, dt.second, - dt.weekday(), 0, -1) - stamp = time.mktime(tt) - tt = time.localtime(stamp) - return tt.tm_isdst > 0 - - -LOCALTZ = LocalTimezone() -"""`tzinfo` object for local time-zone. - -:type: `tzinfo` -""" +# -*- 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 codecs +from datetime import timedelta, tzinfo +import os +import re +try: + set = set +except NameError: + from sets import Set as set +import textwrap +import time +from itertools import izip, imap +missing = object() + +__all__ = ['distinct', 'pathmatch', 'relpath', 'wraptext', 'odict', 'UTC', + 'LOCALTZ'] +__docformat__ = 'restructuredtext en' + + +def distinct(iterable): + """Yield all items in an iterable collection that are distinct. + + Unlike when using sets for a similar effect, the original ordering of the + items in the collection is preserved by this function. + + >>> print list(distinct([1, 2, 1, 3, 4, 4])) + [1, 2, 3, 4] + >>> print list(distinct('foobar')) + ['f', 'o', 'b', 'a', 'r'] + + :param iterable: the iterable collection providing the data + :return: the distinct items in the collection + :rtype: ``iterator`` + """ + seen = set() + for item in iter(iterable): + if item not in seen: + yield item + seen.add(item) + +# Regexp to match python magic encoding line +PYTHON_MAGIC_COMMENT_re = re.compile( + r'[ \t\f]* \# .* coding[=:][ \t]*([-\w.]+)', re.VERBOSE) +def parse_encoding(fp): + """Deduce the encoding of a source file from magic comment. + + It does this in the same way as the `Python interpreter`__ + + .. __: http://docs.python.org/ref/encodings.html + + The ``fp`` argument should be a seekable file object. + + (From Jeff Dairiki) + """ + pos = fp.tell() + fp.seek(0) + try: + line1 = fp.readline() + has_bom = line1.startswith(codecs.BOM_UTF8) + if has_bom: + line1 = line1[len(codecs.BOM_UTF8):] + + m = PYTHON_MAGIC_COMMENT_re.match(line1) + if not m: + try: + import parser + parser.suite(line1) + except (ImportError, SyntaxError): + # Either it's a real syntax error, in which case the source is + # not valid python source, or line2 is a continuation of line1, + # in which case we don't want to scan line2 for a magic + # comment. + pass + else: + line2 = fp.readline() + m = PYTHON_MAGIC_COMMENT_re.match(line2) + + if has_bom: + if m: + raise SyntaxError( + "python refuses to compile code with both a UTF8 " + "byte-order-mark and a magic encoding comment") + return 'utf_8' + elif m: + return m.group(1) + else: + return None + finally: + fp.seek(pos) + +def pathmatch(pattern, filename): + """Extended pathname pattern matching. + + This function is similar to what is provided by the ``fnmatch`` module in + the Python standard library, but: + + * can match complete (relative or absolute) path names, and not just file + names, and + * also supports a convenience pattern ("**") to match files at any + directory level. + + Examples: + + >>> pathmatch('**.py', 'bar.py') + True + >>> pathmatch('**.py', 'foo/bar/baz.py') + True + >>> pathmatch('**.py', 'templates/index.html') + False + + >>> pathmatch('**/templates/*.html', 'templates/index.html') + True + >>> pathmatch('**/templates/*.html', 'templates/foo/bar.html') + False + + :param pattern: the glob pattern + :param filename: the path name of the file to match against + :return: `True` if the path name matches the pattern, `False` otherwise + :rtype: `bool` + """ + symbols = { + '?': '[^/]', + '?/': '[^/]/', + '*': '[^/]+', + '*/': '[^/]+/', + '**/': '(?:.+/)*?', + '**': '(?:.+/)*?[^/]+', + } + buf = [] + for idx, part in enumerate(re.split('([?*]+/?)', pattern)): + if idx % 2: + buf.append(symbols[part]) + elif part: + buf.append(re.escape(part)) + match = re.match(''.join(buf) + '$', filename.replace(os.sep, '/')) + return match is not None + + +class TextWrapper(textwrap.TextWrapper): + wordsep_re = re.compile( + r'(\s+|' # any whitespace + r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))' # em-dash + ) + + +def wraptext(text, width=70, initial_indent='', subsequent_indent=''): + """Simple wrapper around the ``textwrap.wrap`` function in the standard + library. This version does not wrap lines on hyphens in words. + + :param text: the text to wrap + :param width: the maximum line width + :param initial_indent: string that will be prepended to the first line of + wrapped output + :param subsequent_indent: string that will be prepended to all lines save + the first of wrapped output + :return: a list of lines + :rtype: `list` + """ + wrapper = TextWrapper(width=width, initial_indent=initial_indent, + subsequent_indent=subsequent_indent, + break_long_words=False) + return wrapper.wrap(text) + + +class odict(dict): + """Ordered dict implementation. + + :see: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/107747 + """ + def __init__(self, data=None): + dict.__init__(self, data or {}) + self._keys = dict.keys(self) + + def __delitem__(self, key): + dict.__delitem__(self, key) + self._keys.remove(key) + + def __setitem__(self, key, item): + dict.__setitem__(self, key, item) + if key not in self._keys: + self._keys.append(key) + + def __iter__(self): + return iter(self._keys) + iterkeys = __iter__ + + def clear(self): + dict.clear(self) + self._keys = [] + + def copy(self): + d = odict() + d.update(self) + return d + + def items(self): + return zip(self._keys, self.values()) + + def iteritems(self): + return izip(self._keys, self.itervalues()) + + def keys(self): + return self._keys[:] + + def pop(self, key, default=missing): + if default is missing: + return dict.pop(self, key) + elif key not in self: + return default + self._keys.remove(key) + return dict.pop(self, key, default) + + def popitem(self, key): + self._keys.remove(key) + return dict.popitem(key) + + def setdefault(self, key, failobj = None): + dict.setdefault(self, key, failobj) + if key not in self._keys: + self._keys.append(key) + + def update(self, dict): + for (key, val) in dict.items(): + self[key] = val + + def values(self): + return map(self.get, self._keys) + + def itervalues(self): + return imap(self.get, self._keys) + + +try: + relpath = os.path.relpath +except AttributeError: + def relpath(path, start='.'): + """Compute the relative path to one path from another. + + >>> relpath('foo/bar.txt', '').replace(os.sep, '/') + 'foo/bar.txt' + >>> relpath('foo/bar.txt', 'foo').replace(os.sep, '/') + 'bar.txt' + >>> relpath('foo/bar.txt', 'baz').replace(os.sep, '/') + '../foo/bar.txt' + + :return: the relative path + :rtype: `basestring` + """ + 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) + +try: + from operator import attrgetter, itemgetter +except ImportError: + def itemgetter(name): + def _getitem(obj): + return obj[name] + return _getitem + +try: + ''.rsplit + def rsplit(a_string, sep=None, maxsplit=None): + return a_string.rsplit(sep, maxsplit) +except AttributeError: + def rsplit(a_string, sep=None, maxsplit=None): + parts = a_string.split(sep) + if maxsplit is None or len(parts) <= maxsplit: + return parts + maxsplit_index = len(parts) - maxsplit + non_splitted_part = sep.join(parts[:maxsplit_index]) + splitted = parts[maxsplit_index:] + return [non_splitted_part] + splitted + +ZERO = timedelta(0) + + +class FixedOffsetTimezone(tzinfo): + """Fixed offset in minutes east from UTC.""" + + def __init__(self, offset, name=None): + self._offset = timedelta(minutes=offset) + if name is None: + name = 'Etc/GMT+%d' % offset + self.zone = name + + def __str__(self): + return self.zone + + def __repr__(self): + return '' % (self.zone, self._offset) + + def utcoffset(self, dt): + return self._offset + + def tzname(self, dt): + return self.zone + + def dst(self, dt): + return ZERO + + +try: + from pytz import UTC +except ImportError: + UTC = FixedOffsetTimezone(0, 'UTC') + """`tzinfo` object for UTC (Universal Time). + + :type: `tzinfo` + """ + +STDOFFSET = timedelta(seconds = -time.timezone) +if time.daylight: + DSTOFFSET = timedelta(seconds = -time.altzone) +else: + DSTOFFSET = STDOFFSET + +DSTDIFF = DSTOFFSET - STDOFFSET + + +class LocalTimezone(tzinfo): + + def utcoffset(self, dt): + if self._isdst(dt): + return DSTOFFSET + else: + return STDOFFSET + + def dst(self, dt): + if self._isdst(dt): + return DSTDIFF + else: + return ZERO + + def tzname(self, dt): + return time.tzname[self._isdst(dt)] + + def _isdst(self, dt): + tt = (dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second, + dt.weekday(), 0, -1) + stamp = time.mktime(tt) + tt = time.localtime(stamp) + return tt.tm_isdst > 0 + + +LOCALTZ = LocalTimezone() +"""`tzinfo` object for local time-zone. + +:type: `tzinfo` +""" diff --git a/justice/external/linkedin/helper.py b/justice/external/linkedin/helper.py index 116813f..5802662 100644 --- a/justice/external/linkedin/helper.py +++ b/justice/external/linkedin/helper.py @@ -1,36 +1,36 @@ -import linkedin - -def quick_api(api_key, secret_key): - """ This method helps you get access to linkedin api quickly when using it - from the interpreter. - Notice that this method creates http server and wait for a request, so it - shouldn't be used in real production code - it's just an helper for debugging - - The usage is basically: - api = quick_api(KEY, SECRET) - After you do that, it will print a URL to the screen which you must go in - and allow the access, after you do that, the method will return with the api object. - """ - - api = linkedin.LinkedIn(api_key, secret_key, "http://localhost:8000/") - api.request_token() - print "Put this url in your browser and allow the permissions:" - print api.get_authorize_url() - _wait_for_user_to_enter_browser(api) - api.access_token() - return api - -def _wait_for_user_to_enter_browser(api): - import BaseHTTPServer - class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler): - def do_GET(self): - p = self.path.split("?") - params = {} - if len(p) > 1: - import cgi - params = cgi.parse_qs(p[1], True, True) - api._verifier = params["oauth_verifier"][0] - - server_address = ('', 8000) - httpd = BaseHTTPServer.HTTPServer(server_address, MyHandler) +import linkedin + +def quick_api(api_key, secret_key): + """ This method helps you get access to linkedin api quickly when using it + from the interpreter. + Notice that this method creates http server and wait for a request, so it + shouldn't be used in real production code - it's just an helper for debugging + + The usage is basically: + api = quick_api(KEY, SECRET) + After you do that, it will print a URL to the screen which you must go in + and allow the access, after you do that, the method will return with the api object. + """ + + api = linkedin.LinkedIn(api_key, secret_key, "http://localhost:8000/") + api.request_token() + print "Put this url in your browser and allow the permissions:" + print api.get_authorize_url() + _wait_for_user_to_enter_browser(api) + api.access_token() + return api + +def _wait_for_user_to_enter_browser(api): + import BaseHTTPServer + class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler): + def do_GET(self): + p = self.path.split("?") + params = {} + if len(p) > 1: + import cgi + params = cgi.parse_qs(p[1], True, True) + api._verifier = params["oauth_verifier"][0] + + server_address = ('', 8000) + httpd = BaseHTTPServer.HTTPServer(server_address, MyHandler) httpd.handle_request() \ No newline at end of file diff --git a/justice/external/linkedin/linkedin.py b/justice/external/linkedin/linkedin.py index a641903..facfd6e 100644 --- a/justice/external/linkedin/linkedin.py +++ b/justice/external/linkedin/linkedin.py @@ -1,841 +1,841 @@ -# -*- coding: utf-8 -*- - -####################################################################################### -# Python implementation of LinkedIn OAuth Authorization, Profile and Connection APIs. # -# # -# Author: Ozgur Vatansever # -# Email : ozgurvt@gmail.com # -# LinkedIn Account: http://www.linkedin.com/in/ozgurvt # -####################################################################################### - -__version__ = "1.8.1" - -""" -Provides a Pure Python LinkedIn API Interface. -""" -try: - import sha -except DeprecationWarning, derr: - import hashlib - sha = hashlib.sha1 - -import urllib, time, random, httplib, hmac, binascii, cgi, string -from HTMLParser import HTMLParser - -from model import * - -class OAuthError(Exception): - """ - General OAuth exception, nothing special. - """ - def __init__(self, value): - self.parameter = value - - def __str__(self): - return repr(self.parameter) - - -class Stripper(HTMLParser): - """ - Stripper class that strips HTML entity. - """ - def __init__(self): - HTMLParser.__init__(self) - self.reset() - self.fed = [] - - def handle_data(self, d): - self.fed.append(d) - - def getAlteredData(self): - return ''.join(self.fed) - - -class XMLBuilder(object): - def __init__(self, rootTagName): - self.document = minidom.Document() - self.root = self.document.createElement(rootTagName) - self.document.appendChild(self.root) - - def xml(self): - return self.document.toxml() - - def __unicode__(self): - return self.document.toprettyxml() - - def append_element_to_root(self, element): - self.root.appendChild(element) - - def append_list_of_elements_to_element(self, element, elements): - map(lambda x:element.appendChild(x),elements) - return element - - def create_element(self, tag_name): - return self.document.createElement(str(tag_name)) - - def create_element_with_text_node(self, tag_name, text_node): - text_node = self.document.createTextNode(str(text_node)) - element = self.document.createElement(str(tag_name)) - element.appendChild(text_node) - return element - - def create_elements(self, **elements): - return [self.create_element_with_text_node(tag_name, text_node) for tag_name, text_node in elements.items()] - - - - -class ConnectionError(Exception): - pass - -class LinkedIn(object): - def __init__(self, api_key, api_secret, callback_url, gae = False): - """ - LinkedIn Base class that simply implements LinkedIn OAuth Authorization and LinkedIn APIs such as Profile, Connection vs. - - @ LinkedIn OAuth Authorization - ------------------------------ - In OAuth terminology, there are 2 tokens that we need in order to have permission to perform an API request. - Those are request_token and access_token. Thus, this class basicly intends to wrap methods of OAuth spec. which - are related of gettting request_token and access_token strings. - - @ Important Note: - ----------------- - HMAC-SHA1 hashing algorithm will be used while encrypting a request body of an HTTP request. Other alternatives - such as 'SHA-1' or 'PLAINTEXT' are ignored. - - @Reference for OAuth - -------------------- - Please take a look at the link below if you have a basic knowledge of HTTP protocol - - http://developer.linkedin.com/docs/DOC-1008 - - - Please create an application from the link below if you do not have an API key and secret key yet. - - https://www.linkedin.com/secure/developer - @api_key: Your API key - @api_secret: Your API secret key - @callback_url: the return url when the user grants permission to Consumer. - """ - # Credientials - self.API_ENDPOINT = "api.linkedin.com" - self.BASE_URL = "https://%s" % self.API_ENDPOINT - self.VERSION = "1.0" - - self._api_key = api_key - self._api_secret = api_secret - self._callback_url = callback_url - self._gae = gae # Is it google app engine - self._request_token = None # that comes later - self._access_token = None # that comes later and later - - self._request_token_secret = None - self._access_token_secret = None - - self._verifier = None - self._error = None - - def request_token(self): - """ - Performs the corresponding API which returns the request token in a query string - The POST Querydict must include the following: - * oauth_callback - * oauth_consumer_key - * oauth_nonce - * oauth_signature_method - * oauth_timestamp - * oauth_version - """ - self.clear() - - method = "GET" - relative_url = "/uas/oauth/requestToken" - - query_dict = self._query_dict({"oauth_callback" : self._callback_url}) - - self._calc_signature(self._get_url(relative_url), query_dict, self._request_token_secret, method) - - try: - response = self._https_connection(method, relative_url, query_dict) - except ConnectionError: - return False - - oauth_problem = self._get_value_from_raw_qs("oauth_problem", response) - if oauth_problem: - self._error = oauth_problem - return False - - self._request_token = self._get_value_from_raw_qs("oauth_token", response) - self._request_token_secret = self._get_value_from_raw_qs("oauth_token_secret", response) - return True - - def access_token(self, request_token = None, request_token_secret = None, verifier = None): - """ - Performs the corresponding API which returns the access token in a query string - Accroding to the link (http://developer.linkedin.com/docs/DOC-1008), POST Querydict must include the following: - * oauth_consumer_key - * oauth_nonce - * oauth_signature_method - * oauth_timestamp - * oauth_token (request token) - * oauth_version - """ - self._request_token = request_token and request_token or self._request_token - self._request_token_secret = request_token_secret and request_token_secret or self._request_token_secret - self._verifier = verifier and verifier or self._verifier - # if there is no request token, fail immediately - if self._request_token is None: - raise OAuthError("There is no Request Token. Please perform 'request_token' method and obtain that token first.") - - if self._request_token_secret is None: - raise OAuthError("There is no Request Token Secret. Please perform 'request_token' method and obtain that token first.") - - if self._verifier is None: - raise OAuthError("There is no Verifier Key. Please perform 'request_token' method, redirect user to API authorize page and get the _verifier.") - - method = "GET" - relative_url = "/uas/oauth/accessToken" - query_dict = self._query_dict({ - "oauth_token" : self._request_token, - "oauth_verifier" : self._verifier - }) - - self._calc_signature(self._get_url(relative_url), query_dict, self._request_token_secret, method) - - try: - response = self._https_connection(method, relative_url, query_dict) - except ConnectionError: - return False - - oauth_problem = self._get_value_from_raw_qs("oauth_problem", response) - if oauth_problem: - self._error = oauth_problem - return False - - self._access_token = self._get_value_from_raw_qs("oauth_token", response) - self._access_token_secret = self._get_value_from_raw_qs("oauth_token_secret", response) - return True - - def get_profile(self, member_id = None, url = None, fields=[]): - """ - Gets the public profile for a specific user. It is determined by his/her member id or public url. - If none of them is given, the information og the application's owner are returned. - - If none of them are given, current user's details are fetched. - The argument 'fields' determines how much information will be fetched. - - Examples: - client.get_profile(merber_id = 123, url = None, fields=['first-name', 'last-name']) : fetches the profile of a user whose id is 123. - - client.get_profile(merber_id = None, url = None, fields=['first-name', 'last-name']) : fetches current user's profile - - client.get_profile(member_id = None, 'http://www.linkedin.com/in/ozgurv') : fetches the profile of a user whose profile url is http://www.linkedin.com/in/ozgurv - - @ Returns Profile instance - """ - ################# - # BEGIN ROUTINE # - ################# - # if there is no access token or secret, fail immediately - self._check_tokens() - - # specify the url according to the parameters given - raw_url = "/v1/people/" - if url: - url = self._quote(url) - raw_url = (raw_url + "url=%s:public") % url - elif member_id: - raw_url = (raw_url + "id=%s" % member_id) - else: - raw_url = raw_url + "~" - if url is None: - fields = ":(%s)" % ",".join(fields) if len(fields) > 0 else None - if fields: - raw_url = raw_url + fields - - try: - response = self._do_normal_query(raw_url) - return Profile.create(response) # this creates Profile instance or gives you null - except ConnectionError: - return None - - def get_connections(self, member_id = None, public_url = None, fields=[]): - """ - Fetches the connections of a user whose id is the given member_id or url is the given public_url - If none of the parameters given, the connections of the current user are fetched. - @Returns: a list of Profile instances or an empty list if there is no connection. - - Example urls: - * http://api.linkedin.com/v1/people/~/connections (for current user) - * http://api.linkedin.com/v1/people/id=12345/connections (fetch with member_id) - * http://api.linkedin.com/v1/people/url=http%3A%2F%2Fwww.linkedin.com%2Fin%2Flbeebe/connections (fetch with public_url) - """ - self._check_tokens() - - raw_url = "/v1/people/%s/connections" - if member_id: - raw_url = raw_url % ("id=" + member_id) - elif public_url: - raw_url = raw_url % ("url=" + self._quote(public_url)) - else: - raw_url = raw_url % "~" - fields = ":(%s)" % ",".join(fields) if len(fields) > 0 else None - if fields: - raw_url = raw_url + fields - - try: - response = self._do_normal_query(raw_url) - document = minidom.parseString(response) - connections = document.getElementsByTagName("person") - result = [] - for connection in connections: - profile = Profile.create(connection.toxml()) - if profile is not None: - result.append(profile) - - return result - except ConnectionError: - return None - - def get_search(self, parameters): - """ - Use the Search API to find LinkedIn profiles using keywords, - company, name, or other methods. This returns search results, - which are an array of matching member profiles. Each matching - profile is similar to a mini-profile popup view of LinkedIn - member profiles. - - Request URL Structure: - http://api.linkedin.com/v1/people?keywords=['+' delimited keywords]&name=[first name + last name]&company=[company name]¤t-company=[true|false]&title=[title]¤t-title=[true|false]&industry-code=[industry code]&search-location-type=[I,Y]&country-code=[country code]&postal-code=[postal code]&network=[in|out]&start=[number]&count=[1-10]&sort-criteria=[ctx|endorsers|distance|relevance] - """ - self._check_tokens() - - try: - response = self._do_normal_query("/v1/people", method="GET", params=parameters) - except ConnectionError: - return None - - error = self._parse_error(response) - if error: - self._error = error - return None - - document = minidom.parseString(response) - connections = document.getElementsByTagName("person") - result = [] - for connection in connections: - profile = Profile.create(connection.toxml()) - if profile is not None: - result.append(profile) - return result - - def send_message(self, subject, message, ids = [], send_yourself = False): - """ - Sends a Non-HTML message and subject to the members whose IDs are given as a parameter 'ids'. - If the user gives more than 10 ids, the IDs after 10th ID are ignored. - @Input: string x string x list x bool - @Output: bool - Returns True if successfully sends the message otherwise returns False. - - Important Note: You can send a message at most 10 connections at one time. - - Technical Explanation: - --------------------- - Sends a POST request to the URL 'http://api.linkedin.com/v1/people/~/mailbox'. - The XML that will be sent looks like this: - - - - - - - - {subject} - {message} - - - The resulting XML would be like this: - if result is None or '', it is guaranteed that you sent the message. If there occurs an error, you get the following: - - - ... - ... - ... - ... - - """ - ####################################################################################### - # What we do here is first we need to shorten to ID list to 10 elements just in case. # - # Then we need to strip HTML tags using HTMLParser library. # - # Then we are going to build up the XML body and post the request. # - # According to the response parsed, we return True or False. # - ####################################################################################### - - self._check_tokens() - - # Shorten the list. - ids = ids[:10] - if send_yourself: - ids = ids[:9] - ids.append("~") - - subjectStripper = Stripper() - subjectStripper.feed(subject) - subject = subjectStripper.getAlteredData() - bodyStripper = Stripper() - bodyStripper.feed(message) - body = bodyStripper.getAlteredData() - - # Build up the POST body. - builder = XMLBuilder("mailbox-item") - recipients_element = builder.create_element("recipients") - subject_element = builder.create_element_with_text_node("subject", subject) - body_element = builder.create_element_with_text_node("body", body) - for member_id in ids: - recipient_element = builder.create_element("recipient") - person_element = builder.create_element("person") - person_element.setAttribute("path", "/people/%s" % member_id) - recipient_element.appendChild(person_element) - recipients_element.appendChild(recipient_element) - - builder.append_element_to_root(recipients_element) - builder.append_element_to_root(subject_element) - builder.append_element_to_root(body_element) - body = builder.xml() - - try: - response = self._do_normal_query("/v1/people/~/mailbox", body=body, method="POST") - # If API server sends us a response, we know that there occurs an error. - # So we have to parse the response to make sure what causes the error. - # and let the user know by returning False. - if response: - self._error = self._parse_error(response) - return False - except ConnectionError: - return False - - return True - - def send_invitation(self, subject, message, first_name, last_name, email): - """ - Sends an invitation to the given email address. - This method is very similiar to 'send_message' method. - @input: string x string x string x string x string - @output: bool - """ - - ######################################################################################### - # What we do here is first, we need to check the access token. # - # Then we need to strip HTML tags using the HTMLParser library. # - # Then we are going to build up the XML body and post the request. # - # According to the response parsed, we return True or False. # - ######################################################################################### - - self._check_tokens() - - subjectStripper = Stripper() - subjectStripper.feed(subject) - subject = subjectStripper.getAlteredData() - bodyStripper = Stripper() - bodyStripper.feed(message) - body = bodyStripper.getAlteredData() - - # Build up the POST body. - builder = XMLBuilder("mailbox-item") - recipients_element = builder.create_element("recipients") - subject_element = builder.create_element_with_text_node("subject", subject) - body_element = builder.create_element_with_text_node("body", body) - recipient_element = builder.create_element("recipient") - person_element = builder.create_element("person") - person_element.setAttribute("path", "/people/email=%s" % email) - first_name_element = builder.create_element_with_text_node("first-name", first_name) - last_name_element = builder.create_element_with_text_node("last-name", last_name) - builder.append_list_of_elements_to_element(person_element, [first_name_element, last_name_element]) - recipient_element.appendChild(person_element) - recipients_element.appendChild(recipient_element) - - item_content_element = builder.create_element("item-content") - invitation_request_element = builder.create_element("invitation-request") - connect_type_element = builder.create_element_with_text_node("connect-type", "friend") - invitation_request_element.appendChild(connect_type_element) - item_content_element.appendChild(invitation_request_element) - - - builder.append_element_to_root(recipients_element) - builder.append_element_to_root(subject_element) - builder.append_element_to_root(body_element) - builder.append_element_to_root(item_content_element) - body = builder.xml() - - try: - response = self._do_normal_query("/v1/people/~/mailbox", body=body, method="POST") - # If API server sends us a response, we know that there occurs an error. - # So we have to parse the response to make sure what causes the error. - # and let the user know by returning False. - if response: - self._error = self._parse_error(response) - return False - except ConnectionError: - return False - - return True - - def set_status(self, status_message): - """ - This API is deprecated and should be replaced with the share status of linkedin - - Issues a status of the connected user. There is a 140 character limit on status message. - If it is longer than 140 characters, it is shortened. - ----------- - Usage Rules - * We must use an access token from the user. - * We can only set status for the user who grants us access. - ----------- - @input: string - @output: bool - """ - self._check_tokens() - - # Shorten the message just in case. - status_message = str(status_message) - if len(status_message) > 140: - status_message = status_message[:140] - - # Build up the XML request - builder = XMLBuilder("current-status") - status_node = builder.document.createTextNode(status_message) - builder.root.appendChild(status_node) - body = builder.xml() - - try: - response = self._do_normal_query("/v1/people/~/current-status", body=body, method="PUT") - # If API server sends us a response, we know that there occurs an error. - # So we have to parse the response to make sure what causes the error. - # and let the user know by returning False. - if response: - self._error = self._parse_error(response) - return False - except ConnectionError: - return False - - return True - - def clear_status(self): - """ - This API is deprecated and should be replaced with the share status of linkedin - - Clears the status of the connected user. - ----------- - Usage Rules - * We must use an access token from the user. - * We can only set status for the user who grants us access. - ----------- - @input: none - @output: bool - """ - self._check_tokens() - - try: - response = self._do_normal_query("/v1/people/~/current-status", method="DELETE") - # If API server sends us a response, we know that there occurs an error. - # So we have to parse the response to make sure what causes the error. - # and let the user know by returning False. - if response: - self._error = self._parse_error(response) - return False - except ConnectionError: - return False - - return True - - def share_update(self, comment=None, title=None, submitted_url=None, - submitted_image_url=None, description=None, - visibility="connections-only"): - """ - Use the Share API to have a member share content with their network or with all of LinkedIn - ----------- - Usage Rules - * We must use an access token from the user. - * We can only share items for the user who grants us access. - ----------- - visibility: anyone or connections-only - - @output: bool - - SAMPLE - - - 83% of employers will use social media to hire: 78% LinkedIn, 55% Facebook, 45% Twitter [SF Biz Times] http://bit.ly/cCpeOD - - Survey: Social networks top hiring tool - San Francisco Business Times - http://sanfrancisco.bizjournals.com/sanfrancisco/stories/2010/06/28/daily34.html - http://images.bizjournals.com/travel/cityscapes/thumbs/sm_sanfrancisco.jpg - - - anyone - - - - - """ - self._check_tokens() - - if comment is not None: - comment = str(comment) - if len(comment) > 700: - comment = comment[:700] - - if title is not None: - title = str(title) - if len(title) > 200: - title = title[:200] - - if description is not None: - description = str(description) - if len(description) > 400: - description = description[:400] - - # Build up the XML request - builder = XMLBuilder("share") - - if len(comment) > 0: - comment_element = builder.create_element_with_text_node("comment", comment) - builder.append_element_to_root(comment_element) - - - if (submitted_url is not None) or (title is not None): - content_element = builder.create_element("content") - if submitted_url is not None: - submitted_url_element = builder.create_element_with_text_node("submitted-url", submitted_url) - content_element.appendChild(submitted_url_element) - - # must have url to inlcude image url - if submitted_image_url is not None: - submitted_image_url_element = builder.create_element_with_text_node("submitted-image-url", submitted_image_url) - content_element.appendChild(submitted_image_url_element) - - if title is not None: - title_element = builder.create_element_with_text_node("title", title) - content_element.appendChild(title_element) - - if description is not None: - description_element = builder.create_element_with_text_node("description", description) - content_element.appendChild(description_element) - - builder.append_element_to_root(content_element) - - visibility_element = builder.create_element("visibility") - code_element = builder.create_element_with_text_node("code", visibility) - visibility_element.appendChild(code_element) - - - builder.append_element_to_root(visibility_element) - - body = builder.xml() - - try: - response = self._do_normal_query("/v1/people/~/shares", body=body, method="POST") - # If API server sends us a response, we know that there occurs an error. - # So we have to parse the response to make sure what causes the error. - # and let the user know by returning False. - if response: - self._error = self._parse_error(response) - return False - except ConnectionError: - return False - - return True - - def get_authorize_url(self, request_token = None): - self._request_token = request_token and request_token or self._request_token - if self._request_token is None: - raise OAuthError("OAuth Request Token is NULL. Plase acquire this first.") - return "%s%s?oauth_token=%s" % (self.BASE_URL, "/uas/oauth/authorize", self._request_token) - - def get_error(self): - return self._error - - def clear(self): - self._request_token = None - self._access_token = None - self._verifier = None - - self._request_token_secret = None - self._access_token_secret = None - - self._error = None - - ################################################# - # HELPER FUNCTIONS # - # You do not explicitly use those methods below # - ################################################# - - def _generate_nonce(self, length = 20): - return ''.join([string.letters[random.randint(0, len(string.letters) - 1)] for i in range(length)]) - - def _get_url(self, relative_path): - return self.BASE_URL + relative_path - - def _generate_timestamp(self): - return str(int(time.time())) - - def _quote(self, st): - return urllib.quote(st, safe='~') - - def _utf8(self, st): - return isinstance(st, unicode) and st.encode("utf-8") or str(st) - - def _urlencode(self, query_dict): - keys_and_values = [(self._quote(self._utf8(k)), self._quote(self._utf8(v))) for k,v in query_dict.items()] - keys_and_values.sort() - return '&'.join(['%s=%s' % (k, v) for k, v in keys_and_values]) - - def _get_value_from_raw_qs(self, key, qs): - raw_qs = cgi.parse_qs(qs, keep_blank_values = False) - rs = raw_qs.get(key) - if type(rs) == list: - return rs[0] - else: - return rs - - def _signature_base_string(self, method, uri, query_dict): - return "&".join([self._quote(method), self._quote(uri), self._quote(self._urlencode(query_dict))]) - - def _parse_error(self, str_as_xml): - """ - Helper function in order to get error message from an xml string. - In coming xml can be like this: - - - 404 - 1262186271064 - 0000 - [invalid.property.name]. Couldn't find property with name: first_name - - """ - try: - xmlDocument = minidom.parseString(str_as_xml) - if len(xmlDocument.getElementsByTagName("error")) > 0: - error = xmlDocument.getElementsByTagName("message") - if error: - error = error[0] - return error.childNodes[0].nodeValue - return None - except OAuthError, detail: -# raise detail - raise OAuthError("Invalid XML String given: error: %s" % repr(detail)) - - def _create_oauth_header(self, query_dict): - header = 'OAuth realm="http://api.linkedin.com", ' - header += ", ".join(['%s="%s"' % (k, self._quote(query_dict[k])) - for k in sorted(query_dict)]) - return header - - def _query_dict(self, additional = {}): - query_dict = {"oauth_consumer_key": self._api_key, - "oauth_nonce": self._generate_nonce(), - "oauth_signature_method": "HMAC-SHA1", - "oauth_timestamp": self._generate_timestamp(), - "oauth_version": self.VERSION - } - query_dict.update(additional) - return query_dict - - def _do_normal_query(self, relative_url, body=None, method="GET", params=None): - method = method - query_dict = self._query_dict({"oauth_token" : self._access_token}) - signature_dict = dict(query_dict) - - if (params): - signature_dict.update(params) - - query_dict["oauth_signature"] = self._calc_signature(self._get_url(relative_url), - signature_dict, self._access_token_secret, method, update=False) - - if (params): - relative_url = "%s?%s" % (relative_url, self._urlencode(params)) - - response = self._https_connection(method, relative_url, query_dict, body) - - if (response): - error = self._parse_error(response) - if error: - self._error = error - raise ConnectionError() - - return response - - def _check_tokens(self): - if self._access_token is None: - self._error = "There is no Access Token. Please perform 'access_token' method and obtain that token first." - raise OAuthError(self._error) - if self._access_token_secret is None: - self._error = "There is no Access Token Secret. Please perform 'access_token' method and obtain that token first." - raise OAuthError(self._error) - - def _calc_key(self, token_secret): - key = self._quote(self._api_secret) + "&" - if (token_secret): - key += self._quote(token_secret) - return key - - def _calc_signature(self, url, query_dict, token_secret, method = "GET", update=True): - if token_secret != None: - token_secret = token_secret.encode('ascii') - query_string = self._quote(self._urlencode(query_dict)) - signature_base_string = "&".join([self._quote(method), self._quote(url), query_string]) - hashed = hmac.new(self._calc_key(token_secret), signature_base_string, sha) - signature = binascii.b2a_base64(hashed.digest())[:-1] - if (update): - query_dict["oauth_signature"] = signature - return signature - - def _https_connection(self, method, relative_url, query_dict, body=None): - if (self._gae): - return self._https_connection_gae(method, relative_url, query_dict, body) - else: - return self._https_connection_regular(method, relative_url, query_dict, body) - - def _https_connection_regular(self, method, relative_url, query_dict, body = None): - header = self._create_oauth_header(query_dict) - connection = None - try: - connection = httplib.HTTPSConnection(self.API_ENDPOINT) - connection.request(method, relative_url, body = body, - headers={'Authorization':header}) - response = connection.getresponse() - - if response is None: - self._error = "No HTTP response received." - raise ConnectionError() - return response.read() - finally: - if (connection): - connection.close() - - def _https_connection_gae(self, method, relative_url, query_dict, body = None): - from google.appengine.api import urlfetch - if (method == "GET"): - method = urlfetch.GET - elif (method == "POST"): - method = urlfetch.POST - elif (method == "PUT"): - method = urlfetch.PUT - elif (method == "DELETE"): - method = urlfetch.DELETE - - header = self._create_oauth_header(query_dict) - headers = {'Authorization':header} - if (body): - headers["Content-Type"] = "text/xml" - - url = self._get_url(relative_url) - - rpc = urlfetch.create_rpc(deadline=10.0) - urlfetch.make_fetch_call(rpc, url, method=method, headers=headers, - payload=body) - - return rpc.get_result().content - - ######################## - # END HELPER FUNCTIONS # - ######################## +# -*- coding: utf-8 -*- + +####################################################################################### +# Python implementation of LinkedIn OAuth Authorization, Profile and Connection APIs. # +# # +# Author: Ozgur Vatansever # +# Email : ozgurvt@gmail.com # +# LinkedIn Account: http://www.linkedin.com/in/ozgurvt # +####################################################################################### + +__version__ = "1.8.1" + +""" +Provides a Pure Python LinkedIn API Interface. +""" +try: + import sha +except DeprecationWarning, derr: + import hashlib + sha = hashlib.sha1 + +import urllib, time, random, httplib, hmac, binascii, cgi, string +from HTMLParser import HTMLParser + +from model import * + +class OAuthError(Exception): + """ + General OAuth exception, nothing special. + """ + def __init__(self, value): + self.parameter = value + + def __str__(self): + return repr(self.parameter) + + +class Stripper(HTMLParser): + """ + Stripper class that strips HTML entity. + """ + def __init__(self): + HTMLParser.__init__(self) + self.reset() + self.fed = [] + + def handle_data(self, d): + self.fed.append(d) + + def getAlteredData(self): + return ''.join(self.fed) + + +class XMLBuilder(object): + def __init__(self, rootTagName): + self.document = minidom.Document() + self.root = self.document.createElement(rootTagName) + self.document.appendChild(self.root) + + def xml(self): + return self.document.toxml() + + def __unicode__(self): + return self.document.toprettyxml() + + def append_element_to_root(self, element): + self.root.appendChild(element) + + def append_list_of_elements_to_element(self, element, elements): + map(lambda x:element.appendChild(x),elements) + return element + + def create_element(self, tag_name): + return self.document.createElement(str(tag_name)) + + def create_element_with_text_node(self, tag_name, text_node): + text_node = self.document.createTextNode(str(text_node)) + element = self.document.createElement(str(tag_name)) + element.appendChild(text_node) + return element + + def create_elements(self, **elements): + return [self.create_element_with_text_node(tag_name, text_node) for tag_name, text_node in elements.items()] + + + + +class ConnectionError(Exception): + pass + +class LinkedIn(object): + def __init__(self, api_key, api_secret, callback_url, gae = False): + """ + LinkedIn Base class that simply implements LinkedIn OAuth Authorization and LinkedIn APIs such as Profile, Connection vs. + + @ LinkedIn OAuth Authorization + ------------------------------ + In OAuth terminology, there are 2 tokens that we need in order to have permission to perform an API request. + Those are request_token and access_token. Thus, this class basicly intends to wrap methods of OAuth spec. which + are related of gettting request_token and access_token strings. + + @ Important Note: + ----------------- + HMAC-SHA1 hashing algorithm will be used while encrypting a request body of an HTTP request. Other alternatives + such as 'SHA-1' or 'PLAINTEXT' are ignored. + + @Reference for OAuth + -------------------- + Please take a look at the link below if you have a basic knowledge of HTTP protocol + - http://developer.linkedin.com/docs/DOC-1008 + + + Please create an application from the link below if you do not have an API key and secret key yet. + - https://www.linkedin.com/secure/developer + @api_key: Your API key + @api_secret: Your API secret key + @callback_url: the return url when the user grants permission to Consumer. + """ + # Credientials + self.API_ENDPOINT = "api.linkedin.com" + self.BASE_URL = "https://%s" % self.API_ENDPOINT + self.VERSION = "1.0" + + self._api_key = api_key + self._api_secret = api_secret + self._callback_url = callback_url + self._gae = gae # Is it google app engine + self._request_token = None # that comes later + self._access_token = None # that comes later and later + + self._request_token_secret = None + self._access_token_secret = None + + self._verifier = None + self._error = None + + def request_token(self): + """ + Performs the corresponding API which returns the request token in a query string + The POST Querydict must include the following: + * oauth_callback + * oauth_consumer_key + * oauth_nonce + * oauth_signature_method + * oauth_timestamp + * oauth_version + """ + self.clear() + + method = "GET" + relative_url = "/uas/oauth/requestToken" + + query_dict = self._query_dict({"oauth_callback" : self._callback_url}) + + self._calc_signature(self._get_url(relative_url), query_dict, self._request_token_secret, method) + + try: + response = self._https_connection(method, relative_url, query_dict) + except ConnectionError: + return False + + oauth_problem = self._get_value_from_raw_qs("oauth_problem", response) + if oauth_problem: + self._error = oauth_problem + return False + + self._request_token = self._get_value_from_raw_qs("oauth_token", response) + self._request_token_secret = self._get_value_from_raw_qs("oauth_token_secret", response) + return True + + def access_token(self, request_token = None, request_token_secret = None, verifier = None): + """ + Performs the corresponding API which returns the access token in a query string + Accroding to the link (http://developer.linkedin.com/docs/DOC-1008), POST Querydict must include the following: + * oauth_consumer_key + * oauth_nonce + * oauth_signature_method + * oauth_timestamp + * oauth_token (request token) + * oauth_version + """ + self._request_token = request_token and request_token or self._request_token + self._request_token_secret = request_token_secret and request_token_secret or self._request_token_secret + self._verifier = verifier and verifier or self._verifier + # if there is no request token, fail immediately + if self._request_token is None: + raise OAuthError("There is no Request Token. Please perform 'request_token' method and obtain that token first.") + + if self._request_token_secret is None: + raise OAuthError("There is no Request Token Secret. Please perform 'request_token' method and obtain that token first.") + + if self._verifier is None: + raise OAuthError("There is no Verifier Key. Please perform 'request_token' method, redirect user to API authorize page and get the _verifier.") + + method = "GET" + relative_url = "/uas/oauth/accessToken" + query_dict = self._query_dict({ + "oauth_token" : self._request_token, + "oauth_verifier" : self._verifier + }) + + self._calc_signature(self._get_url(relative_url), query_dict, self._request_token_secret, method) + + try: + response = self._https_connection(method, relative_url, query_dict) + except ConnectionError: + return False + + oauth_problem = self._get_value_from_raw_qs("oauth_problem", response) + if oauth_problem: + self._error = oauth_problem + return False + + self._access_token = self._get_value_from_raw_qs("oauth_token", response) + self._access_token_secret = self._get_value_from_raw_qs("oauth_token_secret", response) + return True + + def get_profile(self, member_id = None, url = None, fields=[]): + """ + Gets the public profile for a specific user. It is determined by his/her member id or public url. + If none of them is given, the information og the application's owner are returned. + + If none of them are given, current user's details are fetched. + The argument 'fields' determines how much information will be fetched. + + Examples: + client.get_profile(merber_id = 123, url = None, fields=['first-name', 'last-name']) : fetches the profile of a user whose id is 123. + + client.get_profile(merber_id = None, url = None, fields=['first-name', 'last-name']) : fetches current user's profile + + client.get_profile(member_id = None, 'http://www.linkedin.com/in/ozgurv') : fetches the profile of a user whose profile url is http://www.linkedin.com/in/ozgurv + + @ Returns Profile instance + """ + ################# + # BEGIN ROUTINE # + ################# + # if there is no access token or secret, fail immediately + self._check_tokens() + + # specify the url according to the parameters given + raw_url = "/v1/people/" + if url: + url = self._quote(url) + raw_url = (raw_url + "url=%s:public") % url + elif member_id: + raw_url = (raw_url + "id=%s" % member_id) + else: + raw_url = raw_url + "~" + if url is None: + fields = ":(%s)" % ",".join(fields) if len(fields) > 0 else None + if fields: + raw_url = raw_url + fields + + try: + response = self._do_normal_query(raw_url) + return Profile.create(response) # this creates Profile instance or gives you null + except ConnectionError: + return None + + def get_connections(self, member_id = None, public_url = None, fields=[]): + """ + Fetches the connections of a user whose id is the given member_id or url is the given public_url + If none of the parameters given, the connections of the current user are fetched. + @Returns: a list of Profile instances or an empty list if there is no connection. + + Example urls: + * http://api.linkedin.com/v1/people/~/connections (for current user) + * http://api.linkedin.com/v1/people/id=12345/connections (fetch with member_id) + * http://api.linkedin.com/v1/people/url=http%3A%2F%2Fwww.linkedin.com%2Fin%2Flbeebe/connections (fetch with public_url) + """ + self._check_tokens() + + raw_url = "/v1/people/%s/connections" + if member_id: + raw_url = raw_url % ("id=" + member_id) + elif public_url: + raw_url = raw_url % ("url=" + self._quote(public_url)) + else: + raw_url = raw_url % "~" + fields = ":(%s)" % ",".join(fields) if len(fields) > 0 else None + if fields: + raw_url = raw_url + fields + + try: + response = self._do_normal_query(raw_url) + document = minidom.parseString(response) + connections = document.getElementsByTagName("person") + result = [] + for connection in connections: + profile = Profile.create(connection.toxml()) + if profile is not None: + result.append(profile) + + return result + except ConnectionError: + return None + + def get_search(self, parameters): + """ + Use the Search API to find LinkedIn profiles using keywords, + company, name, or other methods. This returns search results, + which are an array of matching member profiles. Each matching + profile is similar to a mini-profile popup view of LinkedIn + member profiles. + + Request URL Structure: + http://api.linkedin.com/v1/people?keywords=['+' delimited keywords]&name=[first name + last name]&company=[company name]¤t-company=[true|false]&title=[title]¤t-title=[true|false]&industry-code=[industry code]&search-location-type=[I,Y]&country-code=[country code]&postal-code=[postal code]&network=[in|out]&start=[number]&count=[1-10]&sort-criteria=[ctx|endorsers|distance|relevance] + """ + self._check_tokens() + + try: + response = self._do_normal_query("/v1/people", method="GET", params=parameters) + except ConnectionError: + return None + + error = self._parse_error(response) + if error: + self._error = error + return None + + document = minidom.parseString(response) + connections = document.getElementsByTagName("person") + result = [] + for connection in connections: + profile = Profile.create(connection.toxml()) + if profile is not None: + result.append(profile) + return result + + def send_message(self, subject, message, ids = [], send_yourself = False): + """ + Sends a Non-HTML message and subject to the members whose IDs are given as a parameter 'ids'. + If the user gives more than 10 ids, the IDs after 10th ID are ignored. + @Input: string x string x list x bool + @Output: bool + Returns True if successfully sends the message otherwise returns False. + + Important Note: You can send a message at most 10 connections at one time. + + Technical Explanation: + --------------------- + Sends a POST request to the URL 'http://api.linkedin.com/v1/people/~/mailbox'. + The XML that will be sent looks like this: + + + + + + + + {subject} + {message} + + + The resulting XML would be like this: + if result is None or '', it is guaranteed that you sent the message. If there occurs an error, you get the following: + + + ... + ... + ... + ... + + """ + ####################################################################################### + # What we do here is first we need to shorten to ID list to 10 elements just in case. # + # Then we need to strip HTML tags using HTMLParser library. # + # Then we are going to build up the XML body and post the request. # + # According to the response parsed, we return True or False. # + ####################################################################################### + + self._check_tokens() + + # Shorten the list. + ids = ids[:10] + if send_yourself: + ids = ids[:9] + ids.append("~") + + subjectStripper = Stripper() + subjectStripper.feed(subject) + subject = subjectStripper.getAlteredData() + bodyStripper = Stripper() + bodyStripper.feed(message) + body = bodyStripper.getAlteredData() + + # Build up the POST body. + builder = XMLBuilder("mailbox-item") + recipients_element = builder.create_element("recipients") + subject_element = builder.create_element_with_text_node("subject", subject) + body_element = builder.create_element_with_text_node("body", body) + for member_id in ids: + recipient_element = builder.create_element("recipient") + person_element = builder.create_element("person") + person_element.setAttribute("path", "/people/%s" % member_id) + recipient_element.appendChild(person_element) + recipients_element.appendChild(recipient_element) + + builder.append_element_to_root(recipients_element) + builder.append_element_to_root(subject_element) + builder.append_element_to_root(body_element) + body = builder.xml() + + try: + response = self._do_normal_query("/v1/people/~/mailbox", body=body, method="POST") + # If API server sends us a response, we know that there occurs an error. + # So we have to parse the response to make sure what causes the error. + # and let the user know by returning False. + if response: + self._error = self._parse_error(response) + return False + except ConnectionError: + return False + + return True + + def send_invitation(self, subject, message, first_name, last_name, email): + """ + Sends an invitation to the given email address. + This method is very similiar to 'send_message' method. + @input: string x string x string x string x string + @output: bool + """ + + ######################################################################################### + # What we do here is first, we need to check the access token. # + # Then we need to strip HTML tags using the HTMLParser library. # + # Then we are going to build up the XML body and post the request. # + # According to the response parsed, we return True or False. # + ######################################################################################### + + self._check_tokens() + + subjectStripper = Stripper() + subjectStripper.feed(subject) + subject = subjectStripper.getAlteredData() + bodyStripper = Stripper() + bodyStripper.feed(message) + body = bodyStripper.getAlteredData() + + # Build up the POST body. + builder = XMLBuilder("mailbox-item") + recipients_element = builder.create_element("recipients") + subject_element = builder.create_element_with_text_node("subject", subject) + body_element = builder.create_element_with_text_node("body", body) + recipient_element = builder.create_element("recipient") + person_element = builder.create_element("person") + person_element.setAttribute("path", "/people/email=%s" % email) + first_name_element = builder.create_element_with_text_node("first-name", first_name) + last_name_element = builder.create_element_with_text_node("last-name", last_name) + builder.append_list_of_elements_to_element(person_element, [first_name_element, last_name_element]) + recipient_element.appendChild(person_element) + recipients_element.appendChild(recipient_element) + + item_content_element = builder.create_element("item-content") + invitation_request_element = builder.create_element("invitation-request") + connect_type_element = builder.create_element_with_text_node("connect-type", "friend") + invitation_request_element.appendChild(connect_type_element) + item_content_element.appendChild(invitation_request_element) + + + builder.append_element_to_root(recipients_element) + builder.append_element_to_root(subject_element) + builder.append_element_to_root(body_element) + builder.append_element_to_root(item_content_element) + body = builder.xml() + + try: + response = self._do_normal_query("/v1/people/~/mailbox", body=body, method="POST") + # If API server sends us a response, we know that there occurs an error. + # So we have to parse the response to make sure what causes the error. + # and let the user know by returning False. + if response: + self._error = self._parse_error(response) + return False + except ConnectionError: + return False + + return True + + def set_status(self, status_message): + """ + This API is deprecated and should be replaced with the share status of linkedin + + Issues a status of the connected user. There is a 140 character limit on status message. + If it is longer than 140 characters, it is shortened. + ----------- + Usage Rules + * We must use an access token from the user. + * We can only set status for the user who grants us access. + ----------- + @input: string + @output: bool + """ + self._check_tokens() + + # Shorten the message just in case. + status_message = str(status_message) + if len(status_message) > 140: + status_message = status_message[:140] + + # Build up the XML request + builder = XMLBuilder("current-status") + status_node = builder.document.createTextNode(status_message) + builder.root.appendChild(status_node) + body = builder.xml() + + try: + response = self._do_normal_query("/v1/people/~/current-status", body=body, method="PUT") + # If API server sends us a response, we know that there occurs an error. + # So we have to parse the response to make sure what causes the error. + # and let the user know by returning False. + if response: + self._error = self._parse_error(response) + return False + except ConnectionError: + return False + + return True + + def clear_status(self): + """ + This API is deprecated and should be replaced with the share status of linkedin + + Clears the status of the connected user. + ----------- + Usage Rules + * We must use an access token from the user. + * We can only set status for the user who grants us access. + ----------- + @input: none + @output: bool + """ + self._check_tokens() + + try: + response = self._do_normal_query("/v1/people/~/current-status", method="DELETE") + # If API server sends us a response, we know that there occurs an error. + # So we have to parse the response to make sure what causes the error. + # and let the user know by returning False. + if response: + self._error = self._parse_error(response) + return False + except ConnectionError: + return False + + return True + + def share_update(self, comment=None, title=None, submitted_url=None, + submitted_image_url=None, description=None, + visibility="connections-only"): + """ + Use the Share API to have a member share content with their network or with all of LinkedIn + ----------- + Usage Rules + * We must use an access token from the user. + * We can only share items for the user who grants us access. + ----------- + visibility: anyone or connections-only + + @output: bool + + SAMPLE + + + 83% of employers will use social media to hire: 78% LinkedIn, 55% Facebook, 45% Twitter [SF Biz Times] http://bit.ly/cCpeOD + + Survey: Social networks top hiring tool - San Francisco Business Times + http://sanfrancisco.bizjournals.com/sanfrancisco/stories/2010/06/28/daily34.html + http://images.bizjournals.com/travel/cityscapes/thumbs/sm_sanfrancisco.jpg + + + anyone + + + + + """ + self._check_tokens() + + if comment is not None: + comment = str(comment) + if len(comment) > 700: + comment = comment[:700] + + if title is not None: + title = str(title) + if len(title) > 200: + title = title[:200] + + if description is not None: + description = str(description) + if len(description) > 400: + description = description[:400] + + # Build up the XML request + builder = XMLBuilder("share") + + if len(comment) > 0: + comment_element = builder.create_element_with_text_node("comment", comment) + builder.append_element_to_root(comment_element) + + + if (submitted_url is not None) or (title is not None): + content_element = builder.create_element("content") + if submitted_url is not None: + submitted_url_element = builder.create_element_with_text_node("submitted-url", submitted_url) + content_element.appendChild(submitted_url_element) + + # must have url to inlcude image url + if submitted_image_url is not None: + submitted_image_url_element = builder.create_element_with_text_node("submitted-image-url", submitted_image_url) + content_element.appendChild(submitted_image_url_element) + + if title is not None: + title_element = builder.create_element_with_text_node("title", title) + content_element.appendChild(title_element) + + if description is not None: + description_element = builder.create_element_with_text_node("description", description) + content_element.appendChild(description_element) + + builder.append_element_to_root(content_element) + + visibility_element = builder.create_element("visibility") + code_element = builder.create_element_with_text_node("code", visibility) + visibility_element.appendChild(code_element) + + + builder.append_element_to_root(visibility_element) + + body = builder.xml() + + try: + response = self._do_normal_query("/v1/people/~/shares", body=body, method="POST") + # If API server sends us a response, we know that there occurs an error. + # So we have to parse the response to make sure what causes the error. + # and let the user know by returning False. + if response: + self._error = self._parse_error(response) + return False + except ConnectionError: + return False + + return True + + def get_authorize_url(self, request_token = None): + self._request_token = request_token and request_token or self._request_token + if self._request_token is None: + raise OAuthError("OAuth Request Token is NULL. Plase acquire this first.") + return "%s%s?oauth_token=%s" % (self.BASE_URL, "/uas/oauth/authorize", self._request_token) + + def get_error(self): + return self._error + + def clear(self): + self._request_token = None + self._access_token = None + self._verifier = None + + self._request_token_secret = None + self._access_token_secret = None + + self._error = None + + ################################################# + # HELPER FUNCTIONS # + # You do not explicitly use those methods below # + ################################################# + + def _generate_nonce(self, length = 20): + return ''.join([string.letters[random.randint(0, len(string.letters) - 1)] for i in range(length)]) + + def _get_url(self, relative_path): + return self.BASE_URL + relative_path + + def _generate_timestamp(self): + return str(int(time.time())) + + def _quote(self, st): + return urllib.quote(st, safe='~') + + def _utf8(self, st): + return isinstance(st, unicode) and st.encode("utf-8") or str(st) + + def _urlencode(self, query_dict): + keys_and_values = [(self._quote(self._utf8(k)), self._quote(self._utf8(v))) for k,v in query_dict.items()] + keys_and_values.sort() + return '&'.join(['%s=%s' % (k, v) for k, v in keys_and_values]) + + def _get_value_from_raw_qs(self, key, qs): + raw_qs = cgi.parse_qs(qs, keep_blank_values = False) + rs = raw_qs.get(key) + if type(rs) == list: + return rs[0] + else: + return rs + + def _signature_base_string(self, method, uri, query_dict): + return "&".join([self._quote(method), self._quote(uri), self._quote(self._urlencode(query_dict))]) + + def _parse_error(self, str_as_xml): + """ + Helper function in order to get error message from an xml string. + In coming xml can be like this: + + + 404 + 1262186271064 + 0000 + [invalid.property.name]. Couldn't find property with name: first_name + + """ + try: + xmlDocument = minidom.parseString(str_as_xml) + if len(xmlDocument.getElementsByTagName("error")) > 0: + error = xmlDocument.getElementsByTagName("message") + if error: + error = error[0] + return error.childNodes[0].nodeValue + return None + except OAuthError, detail: +# raise detail + raise OAuthError("Invalid XML String given: error: %s" % repr(detail)) + + def _create_oauth_header(self, query_dict): + header = 'OAuth realm="http://api.linkedin.com", ' + header += ", ".join(['%s="%s"' % (k, self._quote(query_dict[k])) + for k in sorted(query_dict)]) + return header + + def _query_dict(self, additional = {}): + query_dict = {"oauth_consumer_key": self._api_key, + "oauth_nonce": self._generate_nonce(), + "oauth_signature_method": "HMAC-SHA1", + "oauth_timestamp": self._generate_timestamp(), + "oauth_version": self.VERSION + } + query_dict.update(additional) + return query_dict + + def _do_normal_query(self, relative_url, body=None, method="GET", params=None): + method = method + query_dict = self._query_dict({"oauth_token" : self._access_token}) + signature_dict = dict(query_dict) + + if (params): + signature_dict.update(params) + + query_dict["oauth_signature"] = self._calc_signature(self._get_url(relative_url), + signature_dict, self._access_token_secret, method, update=False) + + if (params): + relative_url = "%s?%s" % (relative_url, self._urlencode(params)) + + response = self._https_connection(method, relative_url, query_dict, body) + + if (response): + error = self._parse_error(response) + if error: + self._error = error + raise ConnectionError() + + return response + + def _check_tokens(self): + if self._access_token is None: + self._error = "There is no Access Token. Please perform 'access_token' method and obtain that token first." + raise OAuthError(self._error) + if self._access_token_secret is None: + self._error = "There is no Access Token Secret. Please perform 'access_token' method and obtain that token first." + raise OAuthError(self._error) + + def _calc_key(self, token_secret): + key = self._quote(self._api_secret) + "&" + if (token_secret): + key += self._quote(token_secret) + return key + + def _calc_signature(self, url, query_dict, token_secret, method = "GET", update=True): + if token_secret != None: + token_secret = token_secret.encode('ascii') + query_string = self._quote(self._urlencode(query_dict)) + signature_base_string = "&".join([self._quote(method), self._quote(url), query_string]) + hashed = hmac.new(self._calc_key(token_secret), signature_base_string, sha) + signature = binascii.b2a_base64(hashed.digest())[:-1] + if (update): + query_dict["oauth_signature"] = signature + return signature + + def _https_connection(self, method, relative_url, query_dict, body=None): + if (self._gae): + return self._https_connection_gae(method, relative_url, query_dict, body) + else: + return self._https_connection_regular(method, relative_url, query_dict, body) + + def _https_connection_regular(self, method, relative_url, query_dict, body = None): + header = self._create_oauth_header(query_dict) + connection = None + try: + connection = httplib.HTTPSConnection(self.API_ENDPOINT) + connection.request(method, relative_url, body = body, + headers={'Authorization':header}) + response = connection.getresponse() + + if response is None: + self._error = "No HTTP response received." + raise ConnectionError() + return response.read() + finally: + if (connection): + connection.close() + + def _https_connection_gae(self, method, relative_url, query_dict, body = None): + from google.appengine.api import urlfetch + if (method == "GET"): + method = urlfetch.GET + elif (method == "POST"): + method = urlfetch.POST + elif (method == "PUT"): + method = urlfetch.PUT + elif (method == "DELETE"): + method = urlfetch.DELETE + + header = self._create_oauth_header(query_dict) + headers = {'Authorization':header} + if (body): + headers["Content-Type"] = "text/xml" + + url = self._get_url(relative_url) + + rpc = urlfetch.create_rpc(deadline=10.0) + urlfetch.make_fetch_call(rpc, url, method=method, headers=headers, + payload=body) + + return rpc.get_result().content + + ######################## + # END HELPER FUNCTIONS # + ######################## diff --git a/justice/external/linkedin/model.py b/justice/external/linkedin/model.py index 919115a..9037ed5 100644 --- a/justice/external/linkedin/model.py +++ b/justice/external/linkedin/model.py @@ -1,294 +1,294 @@ -import datetime - -from xml.dom import minidom -from xml.sax.saxutils import unescape - -class LinkedInModel: - pass - def __repr__(self): - d = {} - for x,y in self.__dict__.items(): - if (self.__dict__[x]): - d[x] = y - return (self.__module__ + "." + self.__class__.__name__ + " " + - d.__repr__()) - -class Education(LinkedInModel): - """ - Class that wraps an education info of a user - """ - def __init__(self): - self.id = None - self.school_name = None - self.degree = None - self.start_date = None - self.end_date = None - self.activities = None - self.notes = None - self.field_of_study = None - - @staticmethod - def create(node): - """ - - - - - - - - - - - - - - """ - children = node.getElementsByTagName("education") - result = [] - for child in children: - education = Education() - education.id = education._get_child(child, "id") - education.activities = education._get_child(child, "activities") - education.notes = education._get_child(child, "notes") - education.school_name = education._get_child(child, "school-name") - education.degree = education._get_child(child, "degree") - education.field_of_study = education._get_child(child, "field-of-study") - start_date = child.getElementsByTagName("start-date") - if start_date: - start_date = start_date[0] - try: - year = int(education._get_child(start_date, "year")) - education.start_date = datetime.date(year, 1, 1) - month = int(education._get_child(start_date, "month")) - education.start_date = datetime.date(year, month, 1) - except Exception: - pass - - end_date = child.getElementsByTagName("end-date") - if end_date: - end_date = end_date[0] - try: - year = int(education._get_child(end_date, "year")) - education.end_date = datetime.date(year, 1, 1) - month = int(education._get_child(end_date, "month")) - education.end_date = datetime.date(year, month, 1) - except Exception: - pass - - result.append(education) - return result - - def _get_child(self, node, tagName): - try: - domNode = node.getElementsByTagName(tagName)[0] - childNodes = domNode.childNodes - if childNodes: - return childNodes[0].nodeValue - return None - except: - return None - -class Position(LinkedInModel): - """ - Class that wraps a business position info of a user - """ - def __init__(self): - self.id = None - self.title = None - self.summary = None - self.start_date = None - self.end_date = None - self.company = None - - - @staticmethod - def create(node): - """ - - - 101526695 - Developer - - - 2009 - 9 - - true - - Akinon - - - - """ - children = node.getElementsByTagName("position") - result = [] - for child in children: - position = Position() - position.id = position._get_child(child, "id") - position.title = position._get_child(child, "title") - position.summary = position._get_child(child, "summary") - company = child.getElementsByTagName("company") - if company: - company = company[0] - position.company = position._get_child(company, "name") - - start_date = child.getElementsByTagName("start-date") - if start_date: - start_date = start_date[0] - try: - year = int(position._get_child(start_date, "year")) - position.start_date = datetime.date(year, 1, 1) - month = int(position._get_child(start_date, "month")) - position.start_date = datetime.date(year, month, 1) - except Exception: - pass - - end_date = child.getElementsByTagName("end-date") - if end_date: - end_date = end_date[0] - try: - year = int(position._get_child(end_date, "year")) - position.end_date = datetime.date(year, 1, 1) - month = int(position._get_child(end_date, "month")) - position.end_date = datetime.date(year, month, 1) - except Exception: - pass - - result.append(position) - - return result - - - def _get_child(self, node, tagName): - try: - domNode = node.getElementsByTagName(tagName)[0] - childNodes = domNode.childNodes - if childNodes: - return childNodes[0].nodeValue - return None - except: - return None - -class Profile(LinkedInModel): - """ - Wraps the data which comes from Profile API of LinkedIn. - For further information, take a look at LinkedIn Profile API. - """ - def __init__(self): - self.id = None - self.first_name = None - self.last_name = None - self.location = None - self.location_country = None - self.industry = None - self.summary = None - self.specialties = None - self.interests = None - self.honors = None - self.positions = [] - self.educations = [] - self.public_url = None - self.private_url = None - self.picture_url = None - self.current_status = None - self.languages = [] - self.skills = [] - - @staticmethod - def create(xml_string): - """ - @This method is a static method so it shouldn't be called from an instance. - - Parses the given xml string and results in a Profile instance. - If the given instance is not valid, this method returns NULL. - """ - try: - document = minidom.parseString(xml_string) - person = document.getElementsByTagName("person")[0] - profile = Profile() - profile.id = profile._get_child(person, "id") - profile.first_name = profile._get_child(person, "first-name") - profile.last_name = profile._get_child(person, "last-name") - profile.headline = profile._get_child(person, "headline") - profile.specialties = profile._get_child(person, "specialties") - profile.industry = profile._get_child(person, "industry") - profile.honors = profile._get_child(person, "honors") - profile.interests = profile._get_child(person, "interests") - profile.summary = profile._get_child(person, "summary") - profile.picture_url = profile._unescape(profile._get_child(person, "picture-url")) - profile.current_status = profile._get_child(person, "current-status") - profile.public_url = profile._unescape(profile._get_child(person, "public-profile-url")) - - # create location - location = person.getElementsByTagName("location") - if location: - location = location[0] - profile.location = profile._get_child(location, "name") - country = location.getElementsByTagName('country')[0] - profile.location_country = profile._get_child(country, "code") - - private_profile = person.getElementsByTagName("site-standard-profile-request") - if private_profile: - private_profile = private_profile[0] - profile.private_url = profile._get_child(private_profile, "url") - - # create skills - skills = person.getElementsByTagName("skills") - if skills: - skills = skills[0] - children = skills.getElementsByTagName('skill') - for child in children: - if not child.getElementsByTagName('id'): - profile.skills.append(profile._get_child(child, 'name')) - - # create languages - languages = person.getElementsByTagName("languages") - if languages: - languages = languages[0] - children = languages.getElementsByTagName('language') - for child in children: - if not child.getElementsByTagName('id'): - profile.languages.append(profile._get_child(child, 'name')) - - # create positions - positions = person.getElementsByTagName("positions") - if positions: - positions = positions[0] - profile.positions = Position.create(positions) - - # create educations - educations = person.getElementsByTagName("educations") - if educations: - educations = educations[0] - profile.educations = Education.create(educations) - return profile - except: - return None - - return None - - def _unescape(self, url): - if url: - return unescape(url) - return url - - def _get_child(self, node, tagName): - try: - if tagName == "summary": - for n in node.getElementsByTagName(tagName): - if n.parentNode.tagName == node.tagName: - domNode = n - break - else: - domNode = node.getElementsByTagName(tagName)[0] - - if domNode.parentNode.tagName == node.tagName: - childNodes = domNode.childNodes - if childNodes: - return childNodes[0].nodeValue - return None - else: - return None - except: +import datetime + +from xml.dom import minidom +from xml.sax.saxutils import unescape + +class LinkedInModel: + pass + def __repr__(self): + d = {} + for x,y in self.__dict__.items(): + if (self.__dict__[x]): + d[x] = y + return (self.__module__ + "." + self.__class__.__name__ + " " + + d.__repr__()) + +class Education(LinkedInModel): + """ + Class that wraps an education info of a user + """ + def __init__(self): + self.id = None + self.school_name = None + self.degree = None + self.start_date = None + self.end_date = None + self.activities = None + self.notes = None + self.field_of_study = None + + @staticmethod + def create(node): + """ + + + + + + + + + + + + + + """ + children = node.getElementsByTagName("education") + result = [] + for child in children: + education = Education() + education.id = education._get_child(child, "id") + education.activities = education._get_child(child, "activities") + education.notes = education._get_child(child, "notes") + education.school_name = education._get_child(child, "school-name") + education.degree = education._get_child(child, "degree") + education.field_of_study = education._get_child(child, "field-of-study") + start_date = child.getElementsByTagName("start-date") + if start_date: + start_date = start_date[0] + try: + year = int(education._get_child(start_date, "year")) + education.start_date = datetime.date(year, 1, 1) + month = int(education._get_child(start_date, "month")) + education.start_date = datetime.date(year, month, 1) + except Exception: + pass + + end_date = child.getElementsByTagName("end-date") + if end_date: + end_date = end_date[0] + try: + year = int(education._get_child(end_date, "year")) + education.end_date = datetime.date(year, 1, 1) + month = int(education._get_child(end_date, "month")) + education.end_date = datetime.date(year, month, 1) + except Exception: + pass + + result.append(education) + return result + + def _get_child(self, node, tagName): + try: + domNode = node.getElementsByTagName(tagName)[0] + childNodes = domNode.childNodes + if childNodes: + return childNodes[0].nodeValue + return None + except: + return None + +class Position(LinkedInModel): + """ + Class that wraps a business position info of a user + """ + def __init__(self): + self.id = None + self.title = None + self.summary = None + self.start_date = None + self.end_date = None + self.company = None + + + @staticmethod + def create(node): + """ + + + 101526695 + Developer + + + 2009 + 9 + + true + + Akinon + + + + """ + children = node.getElementsByTagName("position") + result = [] + for child in children: + position = Position() + position.id = position._get_child(child, "id") + position.title = position._get_child(child, "title") + position.summary = position._get_child(child, "summary") + company = child.getElementsByTagName("company") + if company: + company = company[0] + position.company = position._get_child(company, "name") + + start_date = child.getElementsByTagName("start-date") + if start_date: + start_date = start_date[0] + try: + year = int(position._get_child(start_date, "year")) + position.start_date = datetime.date(year, 1, 1) + month = int(position._get_child(start_date, "month")) + position.start_date = datetime.date(year, month, 1) + except Exception: + pass + + end_date = child.getElementsByTagName("end-date") + if end_date: + end_date = end_date[0] + try: + year = int(position._get_child(end_date, "year")) + position.end_date = datetime.date(year, 1, 1) + month = int(position._get_child(end_date, "month")) + position.end_date = datetime.date(year, month, 1) + except Exception: + pass + + result.append(position) + + return result + + + def _get_child(self, node, tagName): + try: + domNode = node.getElementsByTagName(tagName)[0] + childNodes = domNode.childNodes + if childNodes: + return childNodes[0].nodeValue + return None + except: + return None + +class Profile(LinkedInModel): + """ + Wraps the data which comes from Profile API of LinkedIn. + For further information, take a look at LinkedIn Profile API. + """ + def __init__(self): + self.id = None + self.first_name = None + self.last_name = None + self.location = None + self.location_country = None + self.industry = None + self.summary = None + self.specialties = None + self.interests = None + self.honors = None + self.positions = [] + self.educations = [] + self.public_url = None + self.private_url = None + self.picture_url = None + self.current_status = None + self.languages = [] + self.skills = [] + + @staticmethod + def create(xml_string): + """ + @This method is a static method so it shouldn't be called from an instance. + + Parses the given xml string and results in a Profile instance. + If the given instance is not valid, this method returns NULL. + """ + try: + document = minidom.parseString(xml_string) + person = document.getElementsByTagName("person")[0] + profile = Profile() + profile.id = profile._get_child(person, "id") + profile.first_name = profile._get_child(person, "first-name") + profile.last_name = profile._get_child(person, "last-name") + profile.headline = profile._get_child(person, "headline") + profile.specialties = profile._get_child(person, "specialties") + profile.industry = profile._get_child(person, "industry") + profile.honors = profile._get_child(person, "honors") + profile.interests = profile._get_child(person, "interests") + profile.summary = profile._get_child(person, "summary") + profile.picture_url = profile._unescape(profile._get_child(person, "picture-url")) + profile.current_status = profile._get_child(person, "current-status") + profile.public_url = profile._unescape(profile._get_child(person, "public-profile-url")) + + # create location + location = person.getElementsByTagName("location") + if location: + location = location[0] + profile.location = profile._get_child(location, "name") + country = location.getElementsByTagName('country')[0] + profile.location_country = profile._get_child(country, "code") + + private_profile = person.getElementsByTagName("site-standard-profile-request") + if private_profile: + private_profile = private_profile[0] + profile.private_url = profile._get_child(private_profile, "url") + + # create skills + skills = person.getElementsByTagName("skills") + if skills: + skills = skills[0] + children = skills.getElementsByTagName('skill') + for child in children: + if not child.getElementsByTagName('id'): + profile.skills.append(profile._get_child(child, 'name')) + + # create languages + languages = person.getElementsByTagName("languages") + if languages: + languages = languages[0] + children = languages.getElementsByTagName('language') + for child in children: + if not child.getElementsByTagName('id'): + profile.languages.append(profile._get_child(child, 'name')) + + # create positions + positions = person.getElementsByTagName("positions") + if positions: + positions = positions[0] + profile.positions = Position.create(positions) + + # create educations + educations = person.getElementsByTagName("educations") + if educations: + educations = educations[0] + profile.educations = Education.create(educations) + return profile + except: + return None + + return None + + def _unescape(self, url): + if url: + return unescape(url) + return url + + def _get_child(self, node, tagName): + try: + if tagName == "summary": + for n in node.getElementsByTagName(tagName): + if n.parentNode.tagName == node.tagName: + domNode = n + break + else: + domNode = node.getElementsByTagName(tagName)[0] + + if domNode.parentNode.tagName == node.tagName: + childNodes = domNode.childNodes + if childNodes: + return childNodes[0].nodeValue + return None + else: + return None + except: return None \ No newline at end of file diff --git a/justice/handlers.py b/justice/handlers.py index c632046..6527731 100644 --- a/justice/handlers.py +++ b/justice/handlers.py @@ -1023,30 +1023,31 @@ def form(self): return forms.ContactForm(self) class LibraryHandler(BaseHandler): - ''' + """ Handler for simulation library - ''' + """ + def get(self): """ Returns a list of available simulations """ - q = models.Simulation.query().order(models.Simulation.name,-models.Simulation.date_uploaded) + q = models.Map.query().order(models.Map.name,-models.Map.date_created) sims = q.fetch() -# q2 = ndb.gql("select name, description, owner, date_uploades from Simulation") +# q2 = ndb.gql("select name, description, creator, date_created from Map") # sims = q2.fetch() params = { 'sims': sims, } - return self.render_template('simulations.html',**params) + return self.render_template('maps.html',**params) class SimulationHandler(BaseHandler): - ''' + """ Handler for simulation viewing - ''' + """ - def get(self,simulation_id): + def get(self, simulation_id): """ Returns a simulation object """ diff --git a/justice/models.py b/justice/models.py index 6cb1008..905a677 100644 --- a/justice/models.py +++ b/justice/models.py @@ -149,6 +149,29 @@ def check_unique(cls, user, provider, uid): def open_id_providers(): return [k for k,v in SocialUser.PROVIDERS_INFO.items() if v['uri']] +class Map(ndb.Model): + creator = ndb.KeyProperty(kind=User,required=True) + date_created = ndb.DateProperty(auto_now_add=True) + name = ndb.StringProperty(required=True) + description = ndb.TextProperty() + map = ndb.JsonProperty(compressed=True) # map in GeoJson Format + + + @classmethod + def get_by_creator(cls, creator): + return cls.query(cls.creator == creator).fetch() + + @classmethod + def get_by_creator_and_name(cls, creator, name): + return cls.query(cls.creator == creator, cls.name == name).get() + + @classmethod + def get_by_date(cls, order='asc'): + if order == 'asc': + return cls.query().order(cls.date_created).fetch() + else: + return cls.query().order(-cls.date_created).fetch() + class Simulation(ndb.Model): owner = ndb.KeyProperty(kind=User,required=True) date_uploaded = ndb.DateProperty(auto_now_add=True) diff --git a/justice/routes.py b/justice/routes.py index 8a4c5a7..9e766e6 100644 --- a/justice/routes.py +++ b/justice/routes.py @@ -21,13 +21,13 @@ RedirectRoute('/activation//', handlers.AccountActivationHandler, name='account-activation', strict_slash=True), RedirectRoute('/resend//', handlers.ResendActivationEmailHandler, name='resend-account-activation', strict_slash=True), RedirectRoute('/contact/', handlers.ContactHandler, name='contact', strict_slash=True), - RedirectRoute('/simulations/',handlers.LibraryHandler, name='simulations', strict_slash=True), - RedirectRoute('/simulations/delete//',handlers.SimulationDelete, name='sim-del', strict_slash=True), - RedirectRoute('/simulations/view//',handlers.SimulationHandler, name='sim-view', strict_slash=True), - RedirectRoute('/simulations/view//map/',handlers.SimulationMapHandler, name='sim-map', strict_slash=True), - RedirectRoute('/simulations/view//series/',handlers.SimulationSeriesHandler, name='sim-series', strict_slash=True), - RedirectRoute('/simulations/view//network/',handlers.SimulationNetHandler, name='sim-net', strict_slash=True), - RedirectRoute('/simulations/view//spread/',handlers.SimulationSpreadHandler, name='sim-spread', strict_slash=True), + RedirectRoute('/maps/', handlers.LibraryHandler, name='maps', strict_slash=True), + RedirectRoute('/simulations/delete//', handlers.SimulationDelete, name='sim-del', strict_slash=True), + RedirectRoute('/simulations/view//', handlers.SimulationHandler, name='sim-view', strict_slash=True), + RedirectRoute('/simulations/view//map/', handlers.SimulationMapHandler, name='sim-map', strict_slash=True), + RedirectRoute('/simulations/view//series/', handlers.SimulationSeriesHandler, name='sim-series', strict_slash=True), + RedirectRoute('/simulations/view//network/', handlers.SimulationNetHandler, name='sim-net', strict_slash=True), + RedirectRoute('/simulations/view//spread/', handlers.SimulationSpreadHandler, name='sim-spread', strict_slash=True), RedirectRoute('/simulations/upload/',handlers.UploadHandler, name='carga', strict_slash=True), RedirectRoute('/settings/profile', handlers.EditProfileHandler, name='edit-profile', strict_slash=True), RedirectRoute('/settings/password', handlers.EditPasswordHandler, name='edit-password', strict_slash=True), diff --git a/justice/static/js/libs/jquery_validation/localization/messages_ca.js b/justice/static/js/libs/jquery_validation/localization/messages_ca.js index 90e5f75..9c62412 100644 --- a/justice/static/js/libs/jquery_validation/localization/messages_ca.js +++ b/justice/static/js/libs/jquery_validation/localization/messages_ca.js @@ -1,23 +1,23 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: CA (Catalan; català) - */ -jQuery.extend(jQuery.validator.messages, { - required: "Aquest camp és obligatori.", - remote: "Si us plau, omple aquest camp.", - email: "Si us plau, escriu una adreça de correu-e vàlida", - url: "Si us plau, escriu una URL vàlida.", - date: "Si us plau, escriu una data vàlida.", - dateISO: "Si us plau, escriu una data (ISO) vàlida.", - number: "Si us plau, escriu un número enter vàlid.", - digits: "Si us plau, escriu només dígits.", - creditcard: "Si us plau, escriu un número de tarjeta vàlid.", - equalTo: "Si us plau, escriu el maateix valor de nou.", - accept: "Si us plau, escriu un valor amb una extensió acceptada.", - maxlength: jQuery.validator.format("Si us plau, no escriguis més de {0} caracters."), - minlength: jQuery.validator.format("Si us plau, no escriguis menys de {0} caracters."), - rangelength: jQuery.validator.format("Si us plau, escriu un valor entre {0} i {1} caracters."), - range: jQuery.validator.format("Si us plau, escriu un valor entre {0} i {1}."), - max: jQuery.validator.format("Si us plau, escriu un valor menor o igual a {0}."), - min: jQuery.validator.format("Si us plau, escriu un valor major o igual a {0}.") +/* + * Translated default messages for the jQuery validation plugin. + * Locale: CA (Catalan; català) + */ +jQuery.extend(jQuery.validator.messages, { + required: "Aquest camp és obligatori.", + remote: "Si us plau, omple aquest camp.", + email: "Si us plau, escriu una adreça de correu-e vàlida", + url: "Si us plau, escriu una URL vàlida.", + date: "Si us plau, escriu una data vàlida.", + dateISO: "Si us plau, escriu una data (ISO) vàlida.", + number: "Si us plau, escriu un número enter vàlid.", + digits: "Si us plau, escriu només dígits.", + creditcard: "Si us plau, escriu un número de tarjeta vàlid.", + equalTo: "Si us plau, escriu el maateix valor de nou.", + accept: "Si us plau, escriu un valor amb una extensió acceptada.", + maxlength: jQuery.validator.format("Si us plau, no escriguis més de {0} caracters."), + minlength: jQuery.validator.format("Si us plau, no escriguis menys de {0} caracters."), + rangelength: jQuery.validator.format("Si us plau, escriu un valor entre {0} i {1} caracters."), + range: jQuery.validator.format("Si us plau, escriu un valor entre {0} i {1}."), + max: jQuery.validator.format("Si us plau, escriu un valor menor o igual a {0}."), + min: jQuery.validator.format("Si us plau, escriu un valor major o igual a {0}.") }); \ No newline at end of file diff --git a/justice/static/js/libs/jquery_validation/localization/messages_si.js b/justice/static/js/libs/jquery_validation/localization/messages_si.js index 317022f..b8d8d7d 100644 --- a/justice/static/js/libs/jquery_validation/localization/messages_si.js +++ b/justice/static/js/libs/jquery_validation/localization/messages_si.js @@ -1,23 +1,23 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: SI (Slovenian) - */ -jQuery.extend(jQuery.validator.messages, { - required: "To polje je obvezno.", - remote: "Vpis v tem polju ni v pravi obliki.", - email: "Prosimo, vnesite pravi email naslov.", - url: "Prosimo, vnesite pravi URL.", - date: "Prosimo, vnesite pravi datum.", - dateISO: "Prosimo, vnesite pravi datum (ISO).", - number: "Prosimo, vnesite pravo številko.", - digits: "Prosimo, vnesite samo številke.", - creditcard: "Prosimo, vnesite pravo številko kreditne kartice.", - equalTo: "Prosimo, ponovno vnesite enako vsebino.", - accept: "Prosimo, vnesite vsebino z pravo končnico.", - maxlength: $.validator.format("Prosimo, da ne vnašate več kot {0} znakov."), - minlength: $.validator.format("Prosimo, vnesite vsaj {0} znakov."), - rangelength: $.validator.format("Prosimo, vnesite od {0} do {1} znakov."), - range: $.validator.format("Prosimo, vnesite vrednost med {0} in {1}."), - max: $.validator.format("Prosimo, vnesite vrednost manjšo ali enako {0}."), - min: $.validator.format("Prosimo, vnesite vrednost večjo ali enako {0}.") -}); +/* + * Translated default messages for the jQuery validation plugin. + * Locale: SI (Slovenian) + */ +jQuery.extend(jQuery.validator.messages, { + required: "To polje je obvezno.", + remote: "Vpis v tem polju ni v pravi obliki.", + email: "Prosimo, vnesite pravi email naslov.", + url: "Prosimo, vnesite pravi URL.", + date: "Prosimo, vnesite pravi datum.", + dateISO: "Prosimo, vnesite pravi datum (ISO).", + number: "Prosimo, vnesite pravo številko.", + digits: "Prosimo, vnesite samo številke.", + creditcard: "Prosimo, vnesite pravo številko kreditne kartice.", + equalTo: "Prosimo, ponovno vnesite enako vsebino.", + accept: "Prosimo, vnesite vsebino z pravo končnico.", + maxlength: $.validator.format("Prosimo, da ne vnašate več kot {0} znakov."), + minlength: $.validator.format("Prosimo, vnesite vsaj {0} znakov."), + rangelength: $.validator.format("Prosimo, vnesite od {0} do {1} znakov."), + range: $.validator.format("Prosimo, vnesite vrednost med {0} in {1}."), + max: $.validator.format("Prosimo, vnesite vrednost manjšo ali enako {0}."), + min: $.validator.format("Prosimo, vnesite vrednost večjo ali enako {0}.") +}); diff --git a/justice/templates/base.html b/justice/templates/base.html index aead476..08d89d3 100644 --- a/justice/templates/base.html +++ b/justice/templates/base.html @@ -141,7 +141,7 @@

{% block header_title %}{{ app_name }}{% endblock %}

{% block subnavbar %}