jenkins-bot has submitted this change and it was merged.
Change subject: i18n.input prompt fallback ......................................................................
i18n.input prompt fallback
pywikibot depends on four i18n messages that are only included in git sub-module scripts/i18n, which is not packaged with pywikibot in setup.py. Three uses of i18n messages are in pagegenerators.py, and one is in Page for cosmetic_changes, however the code where the cosmetic_changes i18n message occurs is already disabled when packaged without scripts. The pagegenerators uses are all via i18n.input.
Instead of raising exception ImportError when the i18n git submodule is not available, or packaging scripts and scripts.i18n for the sake of the three pagegenerators i18n messages: - detect missing message package and raise custom exception TranslationError - add a fallback prompt to i18n.input, used by pagegenerators
Bug: 66897 Change-Id: I12518189126d8f929d30ab18bf8597a288d28891 --- M pywikibot/config2.py M pywikibot/exceptions.py M pywikibot/i18n.py M pywikibot/pagegenerators.py M tests/aspects.py A tests/i18n/pywikibot.py M tests/i18n_tests.py M tests/utils.py 8 files changed, 270 insertions(+), 32 deletions(-)
Approvals: XZise: Looks good to me, but someone else must approve Ladsgroup: Looks good to me, approved jenkins-bot: Verified
diff --git a/pywikibot/config2.py b/pywikibot/config2.py index 0343338..5169b0c 100644 --- a/pywikibot/config2.py +++ b/pywikibot/config2.py @@ -350,7 +350,7 @@ userinterface_init_kwargs = {}
# i18n setting for user interface language -# default is config.mylang or 'en' +# default is obtained from L{locale.getdefaultlocale} userinterface_lang = None
# Should we transliterate characters that do not exist in the console diff --git a/pywikibot/exceptions.py b/pywikibot/exceptions.py index 2a8105f..df3ffbd 100644 --- a/pywikibot/exceptions.py +++ b/pywikibot/exceptions.py @@ -12,6 +12,7 @@ - CaptchaError: Captcha is asked and config.solve_captcha == False - Server504Error: Server timed out with HTTP 504 code - PageNotFound: Page not found (deprecated) + - i18n.TranslationError: i18n/l10n message not available
SiteDefinitionError: Site loading problem - UnknownSite: Site does not exist in Family diff --git a/pywikibot/i18n.py b/pywikibot/i18n.py index 85a1055..1be4248 100644 --- a/pywikibot/i18n.py +++ b/pywikibot/i18n.py @@ -4,6 +4,17 @@
Helper functions for both the internal translation system and for TranslateWiki-based translations. + +By default messages are assumed to reside in a package called +'scripts.i18n'. In pywikibot 2.0, that package is not packaged +with pywikibot, and pywikibot 2.0 does not have a hard dependency +on any i18n messages. However, there are three user input questions +in pagegenerators which will use i18 messages if they can be loaded. + +The default message location may be changed by calling +L{set_message_package} with a package name. The package must contain +an __init__.py, and a message bundle called 'pywikibot' containing +messages. See L{twntranslate} for more information on the messages. """ # # (C) Pywikibot team, 2004-2015 @@ -31,7 +42,50 @@ PLURAL_PATTERN = r'{{PLURAL:(?:%()?([^)]*?)(?:)d)?|(.*?)}}'
# Package name for the translation messages -messages_package_name = 'scripts.i18n' +_messages_package_name = 'scripts.i18n' +# Flag to indicate whether translation messages are available +_messages_available = None + + +def set_messages_package(package_name): + """Set the package name where i18n messages are located.""" + global _messages_package_name + global _messages_available + _messages_package_name = package_name + _messages_available = None + + +def messages_available(): + """ + Return False if there are no i18n messages available. + + To determine if messages are available, it looks for the package name + set using L{set_messages_package} for a message bundle called 'pywikibot' + containing messages. + + @rtype: bool + """ + global _messages_available + if _messages_available is not None: + return _messages_available + with warnings.catch_warnings(): + # Ignore 'missing __init__.py' as import looks at the JSON + # directories before loading the python file. + try: + warnings.simplefilter("ignore", ImportWarning) + module = __import__(_messages_package_name, fromlist=['pywikibot']) + except ImportError: + _messages_available = False + return False + + try: + getattr(module, 'pywikibot').msg + except AttributeError: + _messages_available = False + return False + + _messages_available = True + return True
def _altlang(code): @@ -233,22 +287,37 @@ return []
-class TranslationError(Error): +class TranslationError(Error, ImportError):
"""Raised when no correct translation could be found.""" + + # Inherits from ImportError, as this exception is now used + # where previously an ImportError would have been raised, + # and may have been caught by scripts as such.
pass
def _get_messages_bundle(name): """Load all translation messages for a bundle name.""" + exception_message = 'Unknown problem' + with warnings.catch_warnings(): # Ignore 'missing __init__.py' as import looks at the JSON # directories before loading the python file. warnings.simplefilter("ignore", ImportWarning) - transdict = getattr(__import__(messages_package_name, - fromlist=[name]), - name).msg + try: + transdict = getattr(__import__(_messages_package_name, + fromlist=[name]), + name).msg + except ImportError as e: + exception_message = str(e) + + if not transdict: + raise TranslationError( + 'Could not load bundle %s from message package %s: %s' + % (name, _messages_package_name, exception_message)) + return transdict
@@ -379,6 +448,11 @@ @param fallback: Try an alternate language code @type fallback: boolean """ + if not messages_available(): + raise TranslationError( + 'Unable to load messages package %s for bundle %s' + % (_messages_package_name, twtitle)) + package = twtitle.split("-")[0] transdict = _get_messages_bundle(package) code_needed = False @@ -453,7 +527,7 @@ }
>>> from pywikibot import i18n - >>> i18n.messages_package_name = 'tests.i18n' + >>> i18n.set_messages_package('tests.i18n') >>> # use a number >>> str(i18n.twntranslate('en', 'test-plural', 0) % {'num': 'no'}) 'Bot: Changing no pages.' @@ -469,7 +543,6 @@ >>> # use format strings also outside >>> str(i18n.twntranslate('fr', 'test-plural', 10) % {'descr': 'seulement'}) 'Robot: Changer seulement quelques pages.' - >>> i18n.messages_package_name = 'scripts.i18n'
The translations are retrieved from i18n.<package>, based on the callers import table. @@ -513,6 +586,11 @@ """ package = twtitle.split("-")[0] transdict = _get_messages_bundle(package) + if not transdict: + pywikibot.warning('twhas_key: Could not load message bundle %s.%s' + % (_messages_package_name, package)) + return False + # If a site is given instead of a code, use its language if hasattr(code, 'code'): code = code.code @@ -530,7 +608,7 @@ return (lang for lang in sorted(transdict.keys()) if lang != 'qqq')
-def input(twtitle, parameters=None, password=False): +def input(twtitle, parameters=None, password=False, fallback_prompt=None): """ Ask the user a question, return the user's answer.
@@ -541,9 +619,19 @@ @param twtitle: The TranslateWiki string title, in <package>-<key> format @param parameters: The values which will be applied to the translated text @param password: Hides the user's input (for password entry) + @param fallback_prompt: The English prompt if i18n is not available. @rtype: unicode string """ - code = config.userinterface_lang or \ - locale.getdefaultlocale()[0].split('_')[0] - trans = twtranslate(code, twtitle, parameters) - return pywikibot.input(trans, password) + if not messages_available(): + if not fallback_prompt: + raise TranslationError( + 'Unable to load messages package %s for bundle %s' + % (_messages_package_name, twtitle)) + else: + prompt = fallback_prompt + else: + code = config.userinterface_lang or \ + locale.getdefaultlocale()[0].split('_')[0] + + prompt = twtranslate(code, twtitle, parameters) + return pywikibot.input(prompt, password) diff --git a/pywikibot/pagegenerators.py b/pywikibot/pagegenerators.py index 008d383..b01ae3f 100644 --- a/pywikibot/pagegenerators.py +++ b/pywikibot/pagegenerators.py @@ -402,7 +402,9 @@ """Return generator based on Category defined by arg and gen_func.""" categoryname = arg.partition(':')[2] if not categoryname: - categoryname = i18n.input('pywikibot-enter-category-name') + categoryname = i18n.input( + 'pywikibot-enter-category-name', + fallback_prompt='Please enter the category name:') categoryname = categoryname.replace('#', '|')
categoryname, sep, startfrom = categoryname.partition('|') @@ -468,7 +470,9 @@ fileLinksPageTitle = arg[11:] if not fileLinksPageTitle: fileLinksPageTitle = i18n.input( - 'pywikibot-enter-file-links-processing') + 'pywikibot-enter-file-links-processing', + fallback_prompt='Links to which file page should be ' + 'processed?') if fileLinksPageTitle.startswith(self.site.namespace(6) + ':'): fileLinksPage = pywikibot.FilePage(self.site, fileLinksPageTitle) @@ -505,7 +509,9 @@ elif arg.startswith('-interwiki'): title = arg[11:] if not title: - title = i18n.input('pywikibot-enter-page-processing') + title = i18n.input( + 'pywikibot-enter-page-processing', + fallback_prompt='Which page should be processed?') page = pywikibot.Page(pywikibot.Link(title, self.site)) gen = InterwikiPageGenerator(page) diff --git a/tests/aspects.py b/tests/aspects.py index 250ffb2..7615736 100644 --- a/tests/aspects.py +++ b/tests/aspects.py @@ -1162,6 +1162,16 @@ if self.orig_pywikibot_dir: os.environ['PYWIKIBOT2_DIR'] = self.orig_pywikibot_dir
+ def _execute(self, args, data_in=None, timeout=0, error=None): + from tests.utils import execute_pwb + + site = self.get_site() + + args = args + ['-family:' + site.family.name, + '-code:' + site.code] + + return execute_pwb(args, data_in, timeout, error) +
class DebugOnlyTestCase(TestCase):
diff --git a/tests/i18n/pywikibot.py b/tests/i18n/pywikibot.py new file mode 100644 index 0000000..a8650f3 --- /dev/null +++ b/tests/i18n/pywikibot.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +"""i18n message bundle called 'pywikibot' to fool the i18n loader.""" +msg = {} diff --git a/tests/i18n_tests.py b/tests/i18n_tests.py index f5dc65f..5c05011 100644 --- a/tests/i18n_tests.py +++ b/tests/i18n_tests.py @@ -7,9 +7,16 @@ # __version__ = '$Id$'
-from pywikibot import i18n +import sys
-from tests.aspects import unittest, TestCase +import pywikibot + +from pywikibot import i18n, bot + +from tests.aspects import unittest, TestCase, DefaultSiteTestCase, PwbTestCase + +if sys.version_info[0] == 3: + basestring = (str, )
class TestTranslate(TestCase): @@ -76,25 +83,64 @@ u'test-no-english JA')
-class TestTWN(TestCase): +class UserInterfaceLangTestCase(TestCase): + + """Base class for tests using config.userinterface_lang.""" + + def setUp(self): + super(UserInterfaceLangTestCase, self).setUp() + self.orig_userinterface_lang = pywikibot.config.userinterface_lang + pywikibot.config.userinterface_lang = self.get_site().code + + def tearDown(self): + pywikibot.config.userinterface_lang = self.orig_userinterface_lang + super(UserInterfaceLangTestCase, self).tearDown() + + +class TWNSetMessagePackageBase(TestCase): + + """Partial base class for TranslateWiki tests.""" + + message_package = None + + def setUp(self): + self.orig_messages_package_name = i18n._messages_package_name + i18n.set_messages_package(self.message_package) + super(TWNSetMessagePackageBase, self).setUp() + + def tearDown(self): + super(TWNSetMessagePackageBase, self).tearDown() + i18n.set_messages_package(self.orig_messages_package_name) + + +class TWNTestCaseBase(TWNSetMessagePackageBase):
"""Base class for TranslateWiki tests."""
- net = False - - def setUp(self): - self.orig_messages_package_name = i18n.messages_package_name - i18n.messages_package_name = 'tests.i18n' - super(TestTWN, self).setUp() - - def tearDown(self): - super(TestTWN, self).tearDown() - i18n.messages_package_name = self.orig_messages_package_name + @classmethod + def setUpClass(cls): + if not isinstance(cls.message_package, basestring): + raise TypeError('%s.message_package must be a package name' + % cls.__name__) + # Th call to set_messages_package below exists only to confirm + # that the package exists and messages are available, so + # that tests can be skipped if the i18n data doesnt exist. + cls.orig_messages_package_name = i18n._messages_package_name + i18n.set_messages_package(cls.message_package) + has_messages = i18n.messages_available() + i18n._messages_package_name = cls.orig_messages_package_name + if not has_messages: + raise unittest.SkipTest("i18n messages package '%s' not available." + % cls.message_package) + super(TWNTestCaseBase, cls).setUpClass()
-class TestTWTranslate(TestTWN): +class TestTWTranslate(TWNTestCaseBase):
"""Test twtranslate method.""" + + net = False + message_package = 'tests.i18n'
def testLocalized(self): self.assertEqual(i18n.twtranslate('en', 'test-localized'), @@ -123,12 +169,16 @@ u'test-non-localized EN')
def testNoEnglish(self): - self.assertRaises(i18n.TranslationError, i18n.twtranslate, 'en', 'test-no-english') + self.assertRaises(i18n.TranslationError, i18n.twtranslate, + 'en', 'test-no-english')
-class TestTWNTranslate(TestTWN): +class TestTWNTranslate(TWNTestCaseBase):
"""Test {{PLURAL:}} support.""" + + net = False + message_package = 'tests.i18n'
def testNumber(self): """Use a number.""" @@ -260,6 +310,83 @@ u'Bot: Ă„ndere 1 Zeile von einer Seite.')
+class ScriptMessagesTestCase(TWNTestCaseBase): + + """Real messages test.""" + + net = False + message_package = 'scripts.i18n' + + def test_basic(self): + """Verify that real messages are able to be loaded.""" + self.assertEqual(i18n.twntranslate('en', 'pywikibot-enter-new-text'), + 'Please enter the new text:') + + def test_missing(self): + """Test a missing message from a real message bundle.""" + self.assertRaises(i18n.TranslationError, + i18n.twntranslate, 'en', 'pywikibot-missing-key') + + +class InputTestCase(TWNTestCaseBase, UserInterfaceLangTestCase, PwbTestCase): + + """Test i18n.input.""" + + family = 'wikipedia' + code = 'arz' + + message_package = 'scripts.i18n' + + @classmethod + def setUpClass(cls): + if cls.code in i18n.twget_keys('pywikibot-enter-category-name'): + raise unittest.SkipTest( + '%s has a translation for %s' + % (cls.code, 'pywikibot-enter-category-name')) + + super(InputTestCase, cls).setUpClass() + + def test_pagegen_i18n_input(self): + """Test i18n.input via .""" + result = self._execute(args=['listpages', '-cat'], + data_in='non-existant-category\n', + timeout=5) + + self.assertIn('Please enter the category name:', result['stderr']) + + +class MissingPackageTestCase(TWNSetMessagePackageBase, + UserInterfaceLangTestCase, + DefaultSiteTestCase): + + """Test misssing messages package.""" + + message_package = 'scripts.foobar.i18n' + + def _capture_output(self, text, *args, **kwargs): + self.output_text = text + + def setUp(self): + super(MissingPackageTestCase, self).setUp() + self.output_text = '' + self.orig_raw_input = bot.ui._raw_input + self.orig_output = bot.ui.output + bot.ui._raw_input = lambda *args, **kwargs: 'dummy input' + bot.ui.output = self._capture_output + + def tearDown(self): + bot.ui._raw_input = self.orig_raw_input + bot.ui.output = self.orig_output + super(MissingPackageTestCase, self).tearDown() + + def test_pagegen_i18n_input(self): + """Test i18n.input falls back with missing message package.""" + rv = i18n.input('pywikibot-enter-category-name', + fallback_prompt='dummy output') + self.assertEqual(rv, 'dummy input') + self.assertIn('dummy output ', self.output_text) + + if __name__ == '__main__': try: unittest.main() diff --git a/tests/utils.py b/tests/utils.py index e9ff62b..ae80633 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -266,6 +266,9 @@ env = os.environ.copy() # sys.path may have been modified by the test runner to load dependencies. env['PYTHONPATH'] = ":".join(sys.path) + # LC_ALL is used by i18n.input as an alternative for userinterface_lang + if pywikibot.config.userinterface_lang: + env['LC_ALL'] = pywikibot.config.userinterface_lang # Set EDITOR to an executable that ignores all arguments and does nothing. if sys.platform == 'win32': env['EDITOR'] = 'call'