Commit 405b7ed0 authored by Godefroid Chapelle's avatar Godefroid Chapelle

merge from gotcha-talz3_backport-branch

backport of TAL fixes from z3

- i18n and metal interactions

- fix handling of nested translations with tal:content/replace and i18n:name

some reformatting to ease comparisons between 2.x and 3
parent 2d3fe91b
...@@ -64,6 +64,7 @@ class TALGenerator: ...@@ -64,6 +64,7 @@ class TALGenerator:
self.source_file = source_file self.source_file = source_file
self.emit("setSourceFile", source_file) self.emit("setSourceFile", source_file)
self.i18nContext = TranslationContext() self.i18nContext = TranslationContext()
self.i18nLevel = 0
def getCode(self): def getCode(self):
assert not self.stack assert not self.stack
...@@ -73,7 +74,7 @@ class TALGenerator: ...@@ -73,7 +74,7 @@ class TALGenerator:
def optimize(self, program): def optimize(self, program):
output = [] output = []
collect = [] collect = []
rawseen = cursor = 0 cursor = 0
if self.xml: if self.xml:
endsep = "/>" endsep = "/>"
else: else:
...@@ -118,7 +119,6 @@ class TALGenerator: ...@@ -118,7 +119,6 @@ class TALGenerator:
output.append(("rawtextOffset", (text, len(text)))) output.append(("rawtextOffset", (text, len(text))))
if opcode != None: if opcode != None:
output.append(self.optimizeArgsList(item)) output.append(self.optimizeArgsList(item))
rawseen = cursor+1
collect = [] collect = []
return self.optimizeCommonTriple(output) return self.optimizeCommonTriple(output)
...@@ -180,9 +180,9 @@ class TALGenerator: ...@@ -180,9 +180,9 @@ class TALGenerator:
output = program[:2] output = program[:2]
prev2, prev1 = output prev2, prev1 = output
for item in program[2:]: for item in program[2:]:
if ( item[0] == "beginScope" if ( item[0] == "beginScope"
and prev1[0] == "setPosition" and prev1[0] == "setPosition"
and prev2[0] == "rawtextColumn"): and prev2[0] == "rawtextColumn"):
position = output.pop()[1] position = output.pop()[1]
text, column = output.pop()[1] text, column = output.pop()[1]
prev1 = None, None prev1 = None, None
...@@ -319,7 +319,7 @@ class TALGenerator: ...@@ -319,7 +319,7 @@ class TALGenerator:
assert key == "structure" assert key == "structure"
self.emit("insertStructure", cexpr, attrDict, program) self.emit("insertStructure", cexpr, attrDict, program)
def emitI18nVariable(self, varname, action, expression): def emitI18nVariable(self, stuff):
# Used for i18n:name attributes. arg is extra information describing # Used for i18n:name attributes. arg is extra information describing
# how the contents of the variable should get filled in, and it will # how the contents of the variable should get filled in, and it will
# either be a 1-tuple or a 2-tuple. If arg[0] is None, then the # either be a 1-tuple or a 2-tuple. If arg[0] is None, then the
...@@ -332,6 +332,7 @@ class TALGenerator: ...@@ -332,6 +332,7 @@ class TALGenerator:
# calculate the contents of the variable, e.g. # calculate the contents of the variable, e.g.
# "I live in <span i18n:name="country" # "I live in <span i18n:name="country"
# tal:replace="here/countryOfOrigin" />" # tal:replace="here/countryOfOrigin" />"
varname, action, expression = stuff
m = _name_rx.match(varname) m = _name_rx.match(varname)
if m is None or m.group() != varname: if m is None or m.group() != varname:
raise TALError("illegal i18n:name: %r" % varname, self.position) raise TALError("illegal i18n:name: %r" % varname, self.position)
...@@ -525,6 +526,11 @@ class TALGenerator: ...@@ -525,6 +526,11 @@ class TALGenerator:
varname = i18ndict.get('name') varname = i18ndict.get('name')
i18ndata = i18ndict.get('data') i18ndata = i18ndict.get('data')
if varname and not self.i18nLevel:
raise I18NError(
"i18n:name can only occur inside a translation unit",
position)
if i18ndata and not msgid: if i18ndata and not msgid:
raise I18NError("i18n:data must be accompanied by i18n:translate", raise I18NError("i18n:data must be accompanied by i18n:translate",
position) position)
...@@ -584,7 +590,7 @@ class TALGenerator: ...@@ -584,7 +590,7 @@ class TALGenerator:
todo["defineSlot"] = defineSlot todo["defineSlot"] = defineSlot
if defineSlot or i18ndict: if defineSlot or i18ndict:
domain = i18ndict.get("domain") or self.i18nContext.domain domain = i18ndict.get("domain") or self.i18nContext.domain
source = i18ndict.get("source") or self.i18nContext.source source = i18ndict.get("source") or self.i18nContext.source
target = i18ndict.get("target") or self.i18nContext.target target = i18ndict.get("target") or self.i18nContext.target
...@@ -627,22 +633,28 @@ class TALGenerator: ...@@ -627,22 +633,28 @@ class TALGenerator:
if repeatWhitespace: if repeatWhitespace:
self.emitText(repeatWhitespace) self.emitText(repeatWhitespace)
if content: if content:
todo["content"] = content if varname:
if replace: todo['i18nvar'] = (varname, I18N_CONTENT, None)
todo["content"] = content
self.pushProgram()
else:
todo["content"] = content
elif replace:
# tal:replace w/ i18n:name has slightly different semantics. What # tal:replace w/ i18n:name has slightly different semantics. What
# we're actually replacing then is the contents of the ${name} # we're actually replacing then is the contents of the ${name}
# placeholder. # placeholder.
if varname: if varname:
todo['i18nvar'] = (varname, replace) todo['i18nvar'] = (varname, I18N_EXPRESSION, replace)
else: else:
todo["replace"] = replace todo["replace"] = replace
self.pushProgram() self.pushProgram()
# i18n:name w/o tal:replace uses the content as the interpolation # i18n:name w/o tal:replace uses the content as the interpolation
# dictionary values # dictionary values
elif varname: elif varname:
todo['i18nvar'] = (varname, None) todo['i18nvar'] = (varname, I18N_REPLACE, None)
self.pushProgram() self.pushProgram()
if msgid is not None: if msgid is not None:
self.i18nLevel += 1
todo['msgid'] = msgid todo['msgid'] = msgid
if i18ndata: if i18ndata:
todo['i18ndata'] = i18ndata todo['i18ndata'] = i18ndata
...@@ -682,10 +694,12 @@ class TALGenerator: ...@@ -682,10 +694,12 @@ class TALGenerator:
self.emitStartTag(name, self.replaceAttrs(attrlist, repldict), isend) self.emitStartTag(name, self.replaceAttrs(attrlist, repldict), isend)
if optTag: if optTag:
self.pushProgram() self.pushProgram()
if content: if content and not varname:
self.pushProgram() self.pushProgram()
if msgid is not None: if msgid is not None:
self.pushProgram() self.pushProgram()
if content and varname:
self.pushProgram()
if todo and position != (None, None): if todo and position != (None, None):
todo["position"] = position todo["position"] = position
self.todoPush(todo) self.todoPush(todo)
...@@ -731,10 +745,7 @@ class TALGenerator: ...@@ -731,10 +745,7 @@ class TALGenerator:
# If there's no tal:content or tal:replace in the tag with the # If there's no tal:content or tal:replace in the tag with the
# i18n:name, tal:replace is the default. # i18n:name, tal:replace is the default.
i18nNameAction = I18N_REPLACE
if content: if content:
if varname:
i18nNameAction = I18N_CONTENT
self.emitSubstitution(content, {}) self.emitSubstitution(content, {})
# If we're looking at an implicit msgid, emit the insertTranslation # If we're looking at an implicit msgid, emit the insertTranslation
# opcode now, so that the end tag doesn't become part of the implicit # opcode now, so that the end tag doesn't become part of the implicit
...@@ -742,8 +753,14 @@ class TALGenerator: ...@@ -742,8 +753,14 @@ class TALGenerator:
# the opcode after the i18nVariable opcode so we can better handle # the opcode after the i18nVariable opcode so we can better handle
# tags with both of them in them (and in the latter case, the contents # tags with both of them in them (and in the latter case, the contents
# would be thrown away for msgid purposes). # would be thrown away for msgid purposes).
if msgid is not None and not varname: #
self.emitTranslation(msgid, i18ndata) # Still, we should emit insertTranslation opcode before i18nVariable
# in case tal:content, i18n:translate and i18n:name in the same tag
if msgid is not None:
if (not varname) or (
varname and (varname[1] == I18N_CONTENT)):
self.emitTranslation(msgid, i18ndata)
self.i18nLevel -= 1
if optTag: if optTag:
self.emitOptTag(name, optTag, isend) self.emitOptTag(name, optTag, isend)
elif not isend: elif not isend:
...@@ -760,20 +777,24 @@ class TALGenerator: ...@@ -760,20 +777,24 @@ class TALGenerator:
if replace: if replace:
self.emitSubstitution(replace, repldict) self.emitSubstitution(replace, repldict)
elif varname: elif varname:
if varname[1] is not None:
i18nNameAction = I18N_EXPRESSION
# o varname[0] is the variable name # o varname[0] is the variable name
# o i18nNameAction is either # o varname[1] is either
# - I18N_REPLACE for implicit tal:replace # - I18N_REPLACE for implicit tal:replace
# - I18N_CONTENT for tal:content # - I18N_CONTENT for tal:content
# - I18N_EXPRESSION for explicit tal:replace # - I18N_EXPRESSION for explicit tal:replace
# o varname[1] will be None for the first two actions and the # o varname[2] will be None for the first two actions and the
# replacement tal expression for the third action. # replacement tal expression for the third action.
self.emitI18nVariable(varname[0], i18nNameAction, varname[1]) assert (varname[1]
in [I18N_REPLACE, I18N_CONTENT, I18N_EXPRESSION])
self.emitI18nVariable(varname)
# Do not test for "msgid is not None", i.e. we only want to test for # Do not test for "msgid is not None", i.e. we only want to test for
# explicit msgids here. See comment above. # explicit msgids here. See comment above.
if msgid is not None and varname: if msgid is not None:
self.emitTranslation(msgid, i18ndata) # in case tal:content, i18n:translate and i18n:name in the
# same tag insertTranslation opcode has already been
# emitted
if varname and (varname[1] <> I18N_CONTENT):
self.emitTranslation(msgid, i18ndata)
if repeat: if repeat:
self.emitRepeat(repeat) self.emitRepeat(repeat)
if condition: if condition:
......
...@@ -327,7 +327,7 @@ class TALInterpreter: ...@@ -327,7 +327,7 @@ class TALInterpreter:
name = prefix + "use-macro" name = prefix + "use-macro"
value = macs[-1][0] # Macro name value = macs[-1][0] # Macro name
elif suffix == "define-slot": elif suffix == "define-slot":
name = prefix + "slot" name = prefix + "fill-slot"
elif suffix == "fill-slot": elif suffix == "fill-slot":
pass pass
else: else:
...@@ -418,9 +418,9 @@ class TALInterpreter: ...@@ -418,9 +418,9 @@ class TALInterpreter:
def do_rawtextBeginScope_tal(self, (s, col, position, closeprev, dict)): def do_rawtextBeginScope_tal(self, (s, col, position, closeprev, dict)):
self._stream_write(s) self._stream_write(s)
self.col = col self.col = col
self.position = position
self.engine.setPosition(position)
engine = self.engine engine = self.engine
self.position = position
engine.setPosition(position)
if closeprev: if closeprev:
engine.endScope() engine.endScope()
engine.beginScope() engine.beginScope()
......
<div i18n:translate="">At the tone the time will be
<span i18n:data="here/currentTime"
i18n:translate="timefmt"
i18n:name="time">2:32 pm</span>... beep!</div>
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
<xxx metal:use-macro="INNER3"> <xxx metal:use-macro="INNER3">
<yyy metal:fill-slot="INNERSLOT"> <yyy metal:fill-slot="INNERSLOT">
<zzz metal:define-macro="INSLOT"><aaa metal:slot="null">INSLOT</aaa></zzz> <zzz metal:define-macro="INSLOT">INSLOT</zzz>
</yyy> </yyy>
</xxx> </xxx>
......
<!-- the outer element *must* be tal:something or metal:something -->
<metal:block define-macro="page" i18n:domain="zope">
<title metal:define-slot="title">Z3 UI</title>
</metal:block>
<!-- the outer element *must* include tal:omit-tag='' -->
<x tal:omit-tag="" metal:define-macro="page" i18n:domain="zope">
<title metal:define-slot="title">Z3 UI</title>
</x>
<metal:block define-macro="page">
<html i18:domain="zope">
<metal:block define-slot="title">Z3 UI</metal:block>
</html>
</metal:block>
<html metal:define-macro="page" i18n:domain="zope">
<x metal:define-slot="title" />
</html>
<html metal:use-macro="page" />
\ No newline at end of file
<div>AT THE TONE THE TIME WILL BE 59 MINUTES AFTER 6 PM... BEEP!</div>
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
<span metal:use-macro="OUTER2"> <span metal:use-macro="OUTER2">
AAA AAA
<xxx metal:slot="OUTERSLOT"> <xxx metal:fill-slot="OUTERSLOT">
<span>INNER</span> <span>INNER</span>
</xxx> </xxx>
BBB BBB
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
<span metal:use-macro="OUTER3"> <span metal:use-macro="OUTER3">
AAA AAA
<xxx metal:slot="OUTERSLOT"> <xxx metal:fill-slot="OUTERSLOT">
<span>INNER <span>INNER
<xxx>INNERSLOT</xxx> <xxx>INNERSLOT</xxx>
</span> </span>
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
</span> </span>
<span metal:use-macro="INNER3">INNER <span metal:use-macro="INNER3">INNER
<xxx metal:slot="INNERSLOT">INNERSLOT</xxx> <xxx metal:fill-slot="INNERSLOT">INNERSLOT</xxx>
</span> </span>
<span metal:use-macro="INNER3">INNER <span metal:use-macro="INNER3">INNER
...@@ -72,8 +72,8 @@ ...@@ -72,8 +72,8 @@
<span metal:use-macro="INNER3">INNER <span metal:use-macro="INNER3">INNER
<yyy metal:fill-slot="INNERSLOT"> <yyy metal:fill-slot="INNERSLOT">
<zzz metal:define-macro="INSLOT"><aaa metal:slot="null">INSLOT</aaa></zzz> <zzz metal:define-macro="INSLOT">INSLOT</zzz>
</yyy> </yyy>
</span> </span>
<zzz metal:use-macro="INSLOT"><aaa>INSLOT</aaa></zzz> <zzz metal:use-macro="INSLOT">INSLOT</zzz>
<!-- the outer element *must* be tal:something or metal:something -->
<metal:block define-macro="page" i18n:domain="zope">
<title metal:define-slot="title">Z3 UI</title>
</metal:block>
<!-- the outer element *must* include tal:omit-tag='' -->
<x tal:omit-tag="" metal:define-macro="page" i18n:domain="zope">
<title metal:define-slot="title">Z3 UI</title>
</x>
<metal:block define-macro="page">
<html i18:domain="zope">
<metal:block define-slot="title">Z3 UI</metal:block>
</html>
</metal:block>
<html metal:define-macro="page" i18n:domain="zope">
<x metal:define-slot="title" />
</html>
<html metal:use-macro="page" i18n:domain="zope">
<x metal:fill-slot="title" />
</html>
...@@ -614,6 +614,38 @@ translated string</span> ...@@ -614,6 +614,38 @@ translated string</span>
('rawtextColumn', ('</span>\n', 0)) ('rawtextColumn', ('</span>\n', 0))
]) ])
def test_i18n_name_with_content(self):
self._run_check('<div i18n:translate="">This is text for '
'<span i18n:translate="" tal:content="bar" i18n:name="bar_name"/>.'
'</div>', [
('setPosition', (1, 0)),
('beginScope', {'i18n:translate': ''}),
('startTag', ('div', [('i18n:translate', '', 'i18n')])),
('insertTranslation',
('',
[('rawtextOffset', ('This is text for ', 17)),
('setPosition', (1, 40)),
('beginScope',
{'tal:content': 'bar', 'i18n:name': 'bar_name', 'i18n:translate': ''}),
('i18nVariable',
('bar_name',
[('startTag',
('span',
[('i18n:translate', '', 'i18n'),
('tal:content', 'bar', 'tal'),
('i18n:name', 'bar_name', 'i18n')])),
('insertTranslation',
('',
[('insertText', ('$bar$', []))])),
('rawtextOffset', ('</span>', 7))],
None)),
('endScope', ()),
('rawtextOffset', ('.', 1))])),
('endScope', ()),
('rawtextOffset', ('</div>', 6))
])
def check_i18n_name_implicit_value(self): def check_i18n_name_implicit_value(self):
# input/test22.html # input/test22.html
self._run_check('''\ self._run_check('''\
...@@ -725,31 +757,38 @@ translated string</span> ...@@ -725,31 +757,38 @@ translated string</span>
def check_i18n_data_with_name(self): def check_i18n_data_with_name(self):
# input/test29.html # input/test29.html
self._run_check('''\ self._run_check('''\
At the tone the time will be <div i18n:translate="">At the tone the time will be
<span i18n:data="here/currentTime" <span i18n:data="here/currentTime"
i18n:translate="timefmt" i18n:translate="timefmt"
i18n:name="time">2:32 pm</span>... beep! i18n:name="time">2:32 pm</span>... beep!</div>
''', [ ''',
('rawtextBeginScope', [('setPosition', (1, 0)),
('At the tone the time will be\n', ('beginScope', {'i18n:translate': ''}),
0, ('startTag', ('div', [('i18n:translate', '', 'i18n')])),
(2, 0), ('insertTranslation',
0, ('',
{'i18n:data': 'here/currentTime', [('rawtextBeginScope',
'i18n:name': 'time', ('At the tone the time will be\n',
'i18n:translate': 'timefmt'})), 0,
('insertTranslation', (2, 0),
('timefmt', 0,
[('startTag', {'i18n:data': 'here/currentTime',
('span', 'i18n:name': 'time',
[('i18n:data', 'here/currentTime', 'i18n'), 'i18n:translate': 'timefmt'})),
('i18n:translate', 'timefmt', 'i18n'), ('insertTranslation',
('i18n:name', 'time', 'i18n')])), ('timefmt',
('i18nVariable', ('time', [], None))], [('startTag',
'$here/currentTime$')), ('span',
('endScope', ()), [('i18n:data', 'here/currentTime', 'i18n'),
('rawtextColumn', ('... beep!\n', 0)) ('i18n:translate', 'timefmt', 'i18n'),
]) ('i18n:name', 'time', 'i18n')])),
('i18nVariable', ('time', [], None))],
'$here/currentTime$')),
('endScope', ()),
('rawtextOffset', ('... beep!', 9))])),
('endScope', ()),
('rawtextColumn', ('</div>\n', 0))]
)
def check_i18n_explicit_msgid_with_name(self): def check_i18n_explicit_msgid_with_name(self):
# input/test26.html # input/test26.html
......
...@@ -20,11 +20,11 @@ import unittest ...@@ -20,11 +20,11 @@ import unittest
from StringIO import StringIO from StringIO import StringIO
from TAL.TALDefs import METALError from TAL.TALDefs import METALError, I18NError
from TAL.HTMLTALParser import HTMLTALParser from TAL.HTMLTALParser import HTMLTALParser
from TAL.TALInterpreter import TALInterpreter from TAL.TALInterpreter import TALInterpreter
from TAL.DummyEngine import DummyEngine, DummyTranslationService
from TAL.TALInterpreter import interpolate from TAL.TALInterpreter import interpolate
from TAL.DummyEngine import DummyEngine
class TestCaseBase(unittest.TestCase): class TestCaseBase(unittest.TestCase):
...@@ -60,6 +60,130 @@ class MacroErrorsTestCase(TestCaseBase): ...@@ -60,6 +60,130 @@ class MacroErrorsTestCase(TestCaseBase):
self.macro[0] = ("version", "duh") self.macro[0] = ("version", "duh")
class I18NCornerTestCase(TestCaseBase):
def setUp(self):
self.engine = DummyEngine()
self.engine.setLocal('bar', 'BaRvAlUe')
def _check(self, program, expected):
result = StringIO()
self.interpreter = TALInterpreter(program, {}, self.engine,
stream=result)
self.interpreter()
self.assertEqual(expected, result.getvalue())
def test_content_with_messageid_and_i18nname_and_i18ntranslate(self):
# Let's tell the user this is incredibly silly!
self.assertRaises(
I18NError, self._compile,
'<span i18n:translate="" tal:content="bar" i18n:name="bar_name"/>')
def test_content_with_plaintext_and_i18nname_and_i18ntranslate(self):
# Let's tell the user this is incredibly silly!
self.assertRaises(
I18NError, self._compile,
'<span i18n:translate="" i18n:name="color_name">green</span>')
def test_translate_static_text_as_dynamic(self):
program, macros = self._compile(
'<div i18n:translate="">This is text for '
'<span i18n:translate="" tal:content="bar" i18n:name="bar_name"/>.'
'</div>')
self._check(program,
'<div>THIS IS TEXT FOR <span>BARVALUE</span>.</div>\n')
def test_translate_static_text_as_dynamic_from_bytecode(self):
program = [('version', '1.4'),
('mode', 'html'),
('setPosition', (1, 0)),
('beginScope', {'i18n:translate': ''}),
('startTag', ('div', [('i18n:translate', '', 'i18n')])),
('insertTranslation',
('',
[('rawtextOffset', ('This is text for ', 17)),
('setPosition', (1, 40)),
('beginScope',
{'tal:content': 'bar', 'i18n:name': 'bar_name', 'i18n:translate': ''}),
('i18nVariable',
('bar_name',
[('startTag',
('span',
[('i18n:translate', '', 'i18n'),
('tal:content', 'bar', 'tal'),
('i18n:name', 'bar_name', 'i18n')])),
('insertTranslation',
('',
[('insertText', ('$bar$', []))])),
('rawtextOffset', ('</span>', 7))],
None)),
('endScope', ()),
('rawtextOffset', ('.', 1))])),
('endScope', ()),
('rawtextOffset', ('</div>', 6))
]
self._check(program,
'<div>THIS IS TEXT FOR <span>BARVALUE</span>.</div>\n')
def test_for_correct_msgids(self):
class CollectingTranslationService(DummyTranslationService):
data = []
def translate(self, domain, msgid, mapping=None,
context=None, target_language=None, default=None):
self.data.append(msgid)
return DummyTranslationService.translate(
self,
domain, msgid, mapping, context, target_language, default)
xlatsvc = CollectingTranslationService()
self.engine.translationService = xlatsvc
result = StringIO()
program, macros = self._compile(
'<div i18n:translate="">This is text for '
'<span i18n:translate="" tal:content="bar" '
'i18n:name="bar_name"/>.</div>')
self.interpreter = TALInterpreter(program, {}, self.engine,
stream=result)
self.interpreter()
self.assert_('BaRvAlUe' in xlatsvc.data)
self.assert_('This is text for ${bar_name}.' in
xlatsvc.data)
self.assertEqual(
'<div>THIS IS TEXT FOR <span>BARVALUE</span>.</div>\n',
result.getvalue())
class I18NErrorsTestCase(TestCaseBase):
def _check(self, src, msg):
try:
self._compile(src)
except I18NError:
pass
else:
self.fail(msg)
def test_id_with_replace(self):
self._check('<p i18n:id="foo" tal:replace="string:splat"></p>',
"expected i18n:id with tal:replace to be denied")
def test_missing_values(self):
self._check('<p i18n:attributes=""></p>',
"missing i18n:attributes value not caught")
self._check('<p i18n:data=""></p>',
"missing i18n:data value not caught")
self._check('<p i18n:id=""></p>',
"missing i18n:id value not caught")
def test_id_with_attributes(self):
self._check('''<input name="Delete"
tal:attributes="name string:delete_button"
i18n:attributes="name message-id">''',
"expected attribute being both part of tal:attributes" +
" and having a msgid in i18n:attributes to be denied")
class OutputPresentationTestCase(TestCaseBase): class OutputPresentationTestCase(TestCaseBase):
def check_attribute_wrapping(self): def check_attribute_wrapping(self):
...@@ -159,6 +283,7 @@ def test_suite(): ...@@ -159,6 +283,7 @@ def test_suite():
suite.addTest(unittest.makeSuite(MacroErrorsTestCase, "check_")) suite.addTest(unittest.makeSuite(MacroErrorsTestCase, "check_"))
suite.addTest(unittest.makeSuite(OutputPresentationTestCase, "check_")) suite.addTest(unittest.makeSuite(OutputPresentationTestCase, "check_"))
suite.addTest(unittest.makeSuite(InterpolateTestCase, "check_")) suite.addTest(unittest.makeSuite(InterpolateTestCase, "check_"))
suite.addTest(unittest.makeSuite(I18NCornerTestCase))
return suite return suite
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment