1import unittest 2import string 3from string import Template 4 5 6class ModuleTest(unittest.TestCase): 7 8 def test_attrs(self): 9 # While the exact order of the items in these attributes is not 10 # technically part of the "language spec", in practice there is almost 11 # certainly user code that depends on the order, so de-facto it *is* 12 # part of the spec. 13 self.assertEqual(string.whitespace, ' \t\n\r\x0b\x0c') 14 self.assertEqual(string.ascii_lowercase, 'abcdefghijklmnopqrstuvwxyz') 15 self.assertEqual(string.ascii_uppercase, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') 16 self.assertEqual(string.ascii_letters, string.ascii_lowercase + string.ascii_uppercase) 17 self.assertEqual(string.digits, '0123456789') 18 self.assertEqual(string.hexdigits, string.digits + 'abcdefABCDEF') 19 self.assertEqual(string.octdigits, '01234567') 20 self.assertEqual(string.punctuation, '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~') 21 self.assertEqual(string.printable, string.digits + string.ascii_lowercase + string.ascii_uppercase + string.punctuation + string.whitespace) 22 23 def test_capwords(self): 24 self.assertEqual(string.capwords('abc def ghi'), 'Abc Def Ghi') 25 self.assertEqual(string.capwords('abc\tdef\nghi'), 'Abc Def Ghi') 26 self.assertEqual(string.capwords('abc\t def \nghi'), 'Abc Def Ghi') 27 self.assertEqual(string.capwords('ABC DEF GHI'), 'Abc Def Ghi') 28 self.assertEqual(string.capwords('ABC-DEF-GHI', '-'), 'Abc-Def-Ghi') 29 self.assertEqual(string.capwords('ABC-def DEF-ghi GHI'), 'Abc-def Def-ghi Ghi') 30 self.assertEqual(string.capwords(' aBc DeF '), 'Abc Def') 31 self.assertEqual(string.capwords('\taBc\tDeF\t'), 'Abc Def') 32 self.assertEqual(string.capwords('\taBc\tDeF\t', '\t'), '\tAbc\tDef\t') 33 34 def test_basic_formatter(self): 35 fmt = string.Formatter() 36 self.assertEqual(fmt.format("foo"), "foo") 37 self.assertEqual(fmt.format("foo{0}", "bar"), "foobar") 38 self.assertEqual(fmt.format("foo{1}{0}-{1}", "bar", 6), "foo6bar-6") 39 self.assertRaises(TypeError, fmt.format) 40 self.assertRaises(TypeError, string.Formatter.format) 41 42 def test_format_keyword_arguments(self): 43 fmt = string.Formatter() 44 self.assertEqual(fmt.format("-{arg}-", arg='test'), '-test-') 45 self.assertRaises(KeyError, fmt.format, "-{arg}-") 46 self.assertEqual(fmt.format("-{self}-", self='test'), '-test-') 47 self.assertRaises(KeyError, fmt.format, "-{self}-") 48 self.assertEqual(fmt.format("-{format_string}-", format_string='test'), 49 '-test-') 50 self.assertRaises(KeyError, fmt.format, "-{format_string}-") 51 with self.assertRaisesRegex(TypeError, "format_string"): 52 fmt.format(format_string="-{arg}-", arg='test') 53 54 def test_auto_numbering(self): 55 fmt = string.Formatter() 56 self.assertEqual(fmt.format('foo{}{}', 'bar', 6), 57 'foo{}{}'.format('bar', 6)) 58 self.assertEqual(fmt.format('foo{1}{num}{1}', None, 'bar', num=6), 59 'foo{1}{num}{1}'.format(None, 'bar', num=6)) 60 self.assertEqual(fmt.format('{:^{}}', 'bar', 6), 61 '{:^{}}'.format('bar', 6)) 62 self.assertEqual(fmt.format('{:^{}} {}', 'bar', 6, 'X'), 63 '{:^{}} {}'.format('bar', 6, 'X')) 64 self.assertEqual(fmt.format('{:^{pad}}{}', 'foo', 'bar', pad=6), 65 '{:^{pad}}{}'.format('foo', 'bar', pad=6)) 66 67 with self.assertRaises(ValueError): 68 fmt.format('foo{1}{}', 'bar', 6) 69 70 with self.assertRaises(ValueError): 71 fmt.format('foo{}{1}', 'bar', 6) 72 73 def test_conversion_specifiers(self): 74 fmt = string.Formatter() 75 self.assertEqual(fmt.format("-{arg!r}-", arg='test'), "-'test'-") 76 self.assertEqual(fmt.format("{0!s}", 'test'), 'test') 77 self.assertRaises(ValueError, fmt.format, "{0!h}", 'test') 78 # issue13579 79 self.assertEqual(fmt.format("{0!a}", 42), '42') 80 self.assertEqual(fmt.format("{0!a}", string.ascii_letters), 81 "'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'") 82 self.assertEqual(fmt.format("{0!a}", chr(255)), "'\\xff'") 83 self.assertEqual(fmt.format("{0!a}", chr(256)), "'\\u0100'") 84 85 def test_name_lookup(self): 86 fmt = string.Formatter() 87 class AnyAttr: 88 def __getattr__(self, attr): 89 return attr 90 x = AnyAttr() 91 self.assertEqual(fmt.format("{0.lumber}{0.jack}", x), 'lumberjack') 92 with self.assertRaises(AttributeError): 93 fmt.format("{0.lumber}{0.jack}", '') 94 95 def test_index_lookup(self): 96 fmt = string.Formatter() 97 lookup = ["eggs", "and", "spam"] 98 self.assertEqual(fmt.format("{0[2]}{0[0]}", lookup), 'spameggs') 99 with self.assertRaises(IndexError): 100 fmt.format("{0[2]}{0[0]}", []) 101 with self.assertRaises(KeyError): 102 fmt.format("{0[2]}{0[0]}", {}) 103 104 def test_override_get_value(self): 105 class NamespaceFormatter(string.Formatter): 106 def __init__(self, namespace={}): 107 string.Formatter.__init__(self) 108 self.namespace = namespace 109 110 def get_value(self, key, args, kwds): 111 if isinstance(key, str): 112 try: 113 # Check explicitly passed arguments first 114 return kwds[key] 115 except KeyError: 116 return self.namespace[key] 117 else: 118 string.Formatter.get_value(key, args, kwds) 119 120 fmt = NamespaceFormatter({'greeting':'hello'}) 121 self.assertEqual(fmt.format("{greeting}, world!"), 'hello, world!') 122 123 124 def test_override_format_field(self): 125 class CallFormatter(string.Formatter): 126 def format_field(self, value, format_spec): 127 return format(value(), format_spec) 128 129 fmt = CallFormatter() 130 self.assertEqual(fmt.format('*{0}*', lambda : 'result'), '*result*') 131 132 133 def test_override_convert_field(self): 134 class XFormatter(string.Formatter): 135 def convert_field(self, value, conversion): 136 if conversion == 'x': 137 return None 138 return super().convert_field(value, conversion) 139 140 fmt = XFormatter() 141 self.assertEqual(fmt.format("{0!r}:{0!x}", 'foo', 'foo'), "'foo':None") 142 143 144 def test_override_parse(self): 145 class BarFormatter(string.Formatter): 146 # returns an iterable that contains tuples of the form: 147 # (literal_text, field_name, format_spec, conversion) 148 def parse(self, format_string): 149 for field in format_string.split('|'): 150 if field[0] == '+': 151 # it's markup 152 field_name, _, format_spec = field[1:].partition(':') 153 yield '', field_name, format_spec, None 154 else: 155 yield field, None, None, None 156 157 fmt = BarFormatter() 158 self.assertEqual(fmt.format('*|+0:^10s|*', 'foo'), '* foo *') 159 160 def test_check_unused_args(self): 161 class CheckAllUsedFormatter(string.Formatter): 162 def check_unused_args(self, used_args, args, kwargs): 163 # Track which arguments actually got used 164 unused_args = set(kwargs.keys()) 165 unused_args.update(range(0, len(args))) 166 167 for arg in used_args: 168 unused_args.remove(arg) 169 170 if unused_args: 171 raise ValueError("unused arguments") 172 173 fmt = CheckAllUsedFormatter() 174 self.assertEqual(fmt.format("{0}", 10), "10") 175 self.assertEqual(fmt.format("{0}{i}", 10, i=100), "10100") 176 self.assertEqual(fmt.format("{0}{i}{1}", 10, 20, i=100), "1010020") 177 self.assertRaises(ValueError, fmt.format, "{0}{i}{1}", 10, 20, i=100, j=0) 178 self.assertRaises(ValueError, fmt.format, "{0}", 10, 20) 179 self.assertRaises(ValueError, fmt.format, "{0}", 10, 20, i=100) 180 self.assertRaises(ValueError, fmt.format, "{i}", 10, 20, i=100) 181 182 def test_vformat_recursion_limit(self): 183 fmt = string.Formatter() 184 args = () 185 kwargs = dict(i=100) 186 with self.assertRaises(ValueError) as err: 187 fmt._vformat("{i}", args, kwargs, set(), -1) 188 self.assertIn("recursion", str(err.exception)) 189 190 191# Template tests (formerly housed in test_pep292.py) 192 193class Bag: 194 pass 195 196class Mapping: 197 def __getitem__(self, name): 198 obj = self 199 for part in name.split('.'): 200 try: 201 obj = getattr(obj, part) 202 except AttributeError: 203 raise KeyError(name) 204 return obj 205 206 207class TestTemplate(unittest.TestCase): 208 def test_regular_templates(self): 209 s = Template('$who likes to eat a bag of $what worth $$100') 210 self.assertEqual(s.substitute(dict(who='tim', what='ham')), 211 'tim likes to eat a bag of ham worth $100') 212 self.assertRaises(KeyError, s.substitute, dict(who='tim')) 213 self.assertRaises(TypeError, Template.substitute) 214 215 def test_regular_templates_with_braces(self): 216 s = Template('$who likes ${what} for ${meal}') 217 d = dict(who='tim', what='ham', meal='dinner') 218 self.assertEqual(s.substitute(d), 'tim likes ham for dinner') 219 self.assertRaises(KeyError, s.substitute, 220 dict(who='tim', what='ham')) 221 222 def test_regular_templates_with_upper_case(self): 223 s = Template('$WHO likes ${WHAT} for ${MEAL}') 224 d = dict(WHO='tim', WHAT='ham', MEAL='dinner') 225 self.assertEqual(s.substitute(d), 'tim likes ham for dinner') 226 227 def test_regular_templates_with_non_letters(self): 228 s = Template('$_wh0_ likes ${_w_h_a_t_} for ${mea1}') 229 d = dict(_wh0_='tim', _w_h_a_t_='ham', mea1='dinner') 230 self.assertEqual(s.substitute(d), 'tim likes ham for dinner') 231 232 def test_escapes(self): 233 eq = self.assertEqual 234 s = Template('$who likes to eat a bag of $$what worth $$100') 235 eq(s.substitute(dict(who='tim', what='ham')), 236 'tim likes to eat a bag of $what worth $100') 237 s = Template('$who likes $$') 238 eq(s.substitute(dict(who='tim', what='ham')), 'tim likes $') 239 240 def test_percents(self): 241 eq = self.assertEqual 242 s = Template('%(foo)s $foo ${foo}') 243 d = dict(foo='baz') 244 eq(s.substitute(d), '%(foo)s baz baz') 245 eq(s.safe_substitute(d), '%(foo)s baz baz') 246 247 def test_stringification(self): 248 eq = self.assertEqual 249 s = Template('tim has eaten $count bags of ham today') 250 d = dict(count=7) 251 eq(s.substitute(d), 'tim has eaten 7 bags of ham today') 252 eq(s.safe_substitute(d), 'tim has eaten 7 bags of ham today') 253 s = Template('tim has eaten ${count} bags of ham today') 254 eq(s.substitute(d), 'tim has eaten 7 bags of ham today') 255 256 def test_tupleargs(self): 257 eq = self.assertEqual 258 s = Template('$who ate ${meal}') 259 d = dict(who=('tim', 'fred'), meal=('ham', 'kung pao')) 260 eq(s.substitute(d), "('tim', 'fred') ate ('ham', 'kung pao')") 261 eq(s.safe_substitute(d), "('tim', 'fred') ate ('ham', 'kung pao')") 262 263 def test_SafeTemplate(self): 264 eq = self.assertEqual 265 s = Template('$who likes ${what} for ${meal}') 266 eq(s.safe_substitute(dict(who='tim')), 'tim likes ${what} for ${meal}') 267 eq(s.safe_substitute(dict(what='ham')), '$who likes ham for ${meal}') 268 eq(s.safe_substitute(dict(what='ham', meal='dinner')), 269 '$who likes ham for dinner') 270 eq(s.safe_substitute(dict(who='tim', what='ham')), 271 'tim likes ham for ${meal}') 272 eq(s.safe_substitute(dict(who='tim', what='ham', meal='dinner')), 273 'tim likes ham for dinner') 274 275 def test_invalid_placeholders(self): 276 raises = self.assertRaises 277 s = Template('$who likes $') 278 raises(ValueError, s.substitute, dict(who='tim')) 279 s = Template('$who likes ${what)') 280 raises(ValueError, s.substitute, dict(who='tim')) 281 s = Template('$who likes $100') 282 raises(ValueError, s.substitute, dict(who='tim')) 283 # Template.idpattern should match to only ASCII characters. 284 # https://bugs.python.org/issue31672 285 s = Template("$who likes $\u0131") # (DOTLESS I) 286 raises(ValueError, s.substitute, dict(who='tim')) 287 s = Template("$who likes $\u0130") # (LATIN CAPITAL LETTER I WITH DOT ABOVE) 288 raises(ValueError, s.substitute, dict(who='tim')) 289 290 def test_idpattern_override(self): 291 class PathPattern(Template): 292 idpattern = r'[_a-z][._a-z0-9]*' 293 m = Mapping() 294 m.bag = Bag() 295 m.bag.foo = Bag() 296 m.bag.foo.who = 'tim' 297 m.bag.what = 'ham' 298 s = PathPattern('$bag.foo.who likes to eat a bag of $bag.what') 299 self.assertEqual(s.substitute(m), 'tim likes to eat a bag of ham') 300 301 def test_flags_override(self): 302 class MyPattern(Template): 303 flags = 0 304 s = MyPattern('$wHO likes ${WHAT} for ${meal}') 305 d = dict(wHO='tim', WHAT='ham', meal='dinner', w='fred') 306 self.assertRaises(ValueError, s.substitute, d) 307 self.assertEqual(s.safe_substitute(d), 'fredHO likes ${WHAT} for dinner') 308 309 def test_idpattern_override_inside_outside(self): 310 # bpo-1198569: Allow the regexp inside and outside braces to be 311 # different when deriving from Template. 312 class MyPattern(Template): 313 idpattern = r'[a-z]+' 314 braceidpattern = r'[A-Z]+' 315 flags = 0 316 m = dict(foo='foo', BAR='BAR') 317 s = MyPattern('$foo ${BAR}') 318 self.assertEqual(s.substitute(m), 'foo BAR') 319 320 def test_idpattern_override_inside_outside_invalid_unbraced(self): 321 # bpo-1198569: Allow the regexp inside and outside braces to be 322 # different when deriving from Template. 323 class MyPattern(Template): 324 idpattern = r'[a-z]+' 325 braceidpattern = r'[A-Z]+' 326 flags = 0 327 m = dict(foo='foo', BAR='BAR') 328 s = MyPattern('$FOO') 329 self.assertRaises(ValueError, s.substitute, m) 330 s = MyPattern('${bar}') 331 self.assertRaises(ValueError, s.substitute, m) 332 333 def test_pattern_override(self): 334 class MyPattern(Template): 335 pattern = r""" 336 (?P<escaped>@{2}) | 337 @(?P<named>[_a-z][._a-z0-9]*) | 338 @{(?P<braced>[_a-z][._a-z0-9]*)} | 339 (?P<invalid>@) 340 """ 341 m = Mapping() 342 m.bag = Bag() 343 m.bag.foo = Bag() 344 m.bag.foo.who = 'tim' 345 m.bag.what = 'ham' 346 s = MyPattern('@bag.foo.who likes to eat a bag of @bag.what') 347 self.assertEqual(s.substitute(m), 'tim likes to eat a bag of ham') 348 349 class BadPattern(Template): 350 pattern = r""" 351 (?P<badname>.*) | 352 (?P<escaped>@{2}) | 353 @(?P<named>[_a-z][._a-z0-9]*) | 354 @{(?P<braced>[_a-z][._a-z0-9]*)} | 355 (?P<invalid>@) | 356 """ 357 s = BadPattern('@bag.foo.who likes to eat a bag of @bag.what') 358 self.assertRaises(ValueError, s.substitute, {}) 359 self.assertRaises(ValueError, s.safe_substitute, {}) 360 361 def test_braced_override(self): 362 class MyTemplate(Template): 363 pattern = r""" 364 \$(?: 365 (?P<escaped>$) | 366 (?P<named>[_a-z][_a-z0-9]*) | 367 @@(?P<braced>[_a-z][_a-z0-9]*)@@ | 368 (?P<invalid>) | 369 ) 370 """ 371 372 tmpl = 'PyCon in $@@location@@' 373 t = MyTemplate(tmpl) 374 self.assertRaises(KeyError, t.substitute, {}) 375 val = t.substitute({'location': 'Cleveland'}) 376 self.assertEqual(val, 'PyCon in Cleveland') 377 378 def test_braced_override_safe(self): 379 class MyTemplate(Template): 380 pattern = r""" 381 \$(?: 382 (?P<escaped>$) | 383 (?P<named>[_a-z][_a-z0-9]*) | 384 @@(?P<braced>[_a-z][_a-z0-9]*)@@ | 385 (?P<invalid>) | 386 ) 387 """ 388 389 tmpl = 'PyCon in $@@location@@' 390 t = MyTemplate(tmpl) 391 self.assertEqual(t.safe_substitute(), tmpl) 392 val = t.safe_substitute({'location': 'Cleveland'}) 393 self.assertEqual(val, 'PyCon in Cleveland') 394 395 def test_invalid_with_no_lines(self): 396 # The error formatting for invalid templates 397 # has a special case for no data that the default 398 # pattern can't trigger (always has at least '$') 399 # So we craft a pattern that is always invalid 400 # with no leading data. 401 class MyTemplate(Template): 402 pattern = r""" 403 (?P<invalid>) | 404 unreachable( 405 (?P<named>) | 406 (?P<braced>) | 407 (?P<escaped>) 408 ) 409 """ 410 s = MyTemplate('') 411 with self.assertRaises(ValueError) as err: 412 s.substitute({}) 413 self.assertIn('line 1, col 1', str(err.exception)) 414 415 def test_unicode_values(self): 416 s = Template('$who likes $what') 417 d = dict(who='t\xffm', what='f\xfe\fed') 418 self.assertEqual(s.substitute(d), 't\xffm likes f\xfe\x0ced') 419 420 def test_keyword_arguments(self): 421 eq = self.assertEqual 422 s = Template('$who likes $what') 423 eq(s.substitute(who='tim', what='ham'), 'tim likes ham') 424 eq(s.substitute(dict(who='tim'), what='ham'), 'tim likes ham') 425 eq(s.substitute(dict(who='fred', what='kung pao'), 426 who='tim', what='ham'), 427 'tim likes ham') 428 s = Template('the mapping is $mapping') 429 eq(s.substitute(dict(foo='none'), mapping='bozo'), 430 'the mapping is bozo') 431 eq(s.substitute(dict(mapping='one'), mapping='two'), 432 'the mapping is two') 433 434 s = Template('the self is $self') 435 eq(s.substitute(self='bozo'), 'the self is bozo') 436 437 def test_keyword_arguments_safe(self): 438 eq = self.assertEqual 439 raises = self.assertRaises 440 s = Template('$who likes $what') 441 eq(s.safe_substitute(who='tim', what='ham'), 'tim likes ham') 442 eq(s.safe_substitute(dict(who='tim'), what='ham'), 'tim likes ham') 443 eq(s.safe_substitute(dict(who='fred', what='kung pao'), 444 who='tim', what='ham'), 445 'tim likes ham') 446 s = Template('the mapping is $mapping') 447 eq(s.safe_substitute(dict(foo='none'), mapping='bozo'), 448 'the mapping is bozo') 449 eq(s.safe_substitute(dict(mapping='one'), mapping='two'), 450 'the mapping is two') 451 d = dict(mapping='one') 452 raises(TypeError, s.substitute, d, {}) 453 raises(TypeError, s.safe_substitute, d, {}) 454 455 s = Template('the self is $self') 456 eq(s.safe_substitute(self='bozo'), 'the self is bozo') 457 458 def test_delimiter_override(self): 459 eq = self.assertEqual 460 raises = self.assertRaises 461 class AmpersandTemplate(Template): 462 delimiter = '&' 463 s = AmpersandTemplate('this &gift is for &{who} &&') 464 eq(s.substitute(gift='bud', who='you'), 'this bud is for you &') 465 raises(KeyError, s.substitute) 466 eq(s.safe_substitute(gift='bud', who='you'), 'this bud is for you &') 467 eq(s.safe_substitute(), 'this &gift is for &{who} &') 468 s = AmpersandTemplate('this &gift is for &{who} &') 469 raises(ValueError, s.substitute, dict(gift='bud', who='you')) 470 eq(s.safe_substitute(), 'this &gift is for &{who} &') 471 472 class PieDelims(Template): 473 delimiter = '@' 474 s = PieDelims('@who likes to eat a bag of @{what} worth $100') 475 self.assertEqual(s.substitute(dict(who='tim', what='ham')), 476 'tim likes to eat a bag of ham worth $100') 477 478 479if __name__ == '__main__': 480 unittest.main() 481