1import os 2import sys 3import unittest 4from unittest.mock import patch 5 6import mkdocs 7from mkdocs.config import config_options 8from mkdocs.config.base import Config 9from mkdocs.tests.base import tempdir 10 11 12class OptionallyRequiredTest(unittest.TestCase): 13 14 def test_empty(self): 15 16 option = config_options.OptionallyRequired() 17 value = option.validate(None) 18 self.assertEqual(value, None) 19 20 self.assertEqual(option.is_required(), False) 21 22 def test_required(self): 23 24 option = config_options.OptionallyRequired(required=True) 25 self.assertRaises(config_options.ValidationError, 26 option.validate, None) 27 28 self.assertEqual(option.is_required(), True) 29 30 def test_required_no_default(self): 31 32 option = config_options.OptionallyRequired(required=True) 33 value = option.validate(2) 34 self.assertEqual(2, value) 35 36 def test_default(self): 37 38 option = config_options.OptionallyRequired(default=1) 39 value = option.validate(None) 40 self.assertEqual(1, value) 41 42 def test_replace_default(self): 43 44 option = config_options.OptionallyRequired(default=1) 45 value = option.validate(2) 46 self.assertEqual(2, value) 47 48 49class TypeTest(unittest.TestCase): 50 51 def test_single_type(self): 52 53 option = config_options.Type(str) 54 value = option.validate("Testing") 55 self.assertEqual(value, "Testing") 56 57 def test_multiple_types(self): 58 option = config_options.Type((list, tuple)) 59 60 value = option.validate([1, 2, 3]) 61 self.assertEqual(value, [1, 2, 3]) 62 63 value = option.validate((1, 2, 3)) 64 self.assertEqual(value, (1, 2, 3)) 65 66 self.assertRaises(config_options.ValidationError, 67 option.validate, {'a': 1}) 68 69 def test_length(self): 70 option = config_options.Type(str, length=7) 71 72 value = option.validate("Testing") 73 self.assertEqual(value, "Testing") 74 75 self.assertRaises(config_options.ValidationError, 76 option.validate, "Testing Long") 77 78 79class ChoiceTest(unittest.TestCase): 80 81 def test_valid_choice(self): 82 option = config_options.Choice(('python', 'node')) 83 value = option.validate('python') 84 self.assertEqual(value, 'python') 85 86 def test_invalid_choice(self): 87 option = config_options.Choice(('python', 'node')) 88 self.assertRaises( 89 config_options.ValidationError, option.validate, 'go') 90 91 def test_invalid_choices(self): 92 self.assertRaises(ValueError, config_options.Choice, '') 93 self.assertRaises(ValueError, config_options.Choice, []) 94 self.assertRaises(ValueError, config_options.Choice, 5) 95 96 97class DeprecatedTest(unittest.TestCase): 98 99 def test_deprecated_option_simple(self): 100 option = config_options.Deprecated() 101 option.pre_validation({'d': 'value'}, 'd') 102 self.assertEqual(len(option.warnings), 1) 103 option.validate('value') 104 105 def test_deprecated_option_message(self): 106 msg = 'custom message for {} key' 107 option = config_options.Deprecated(message=msg) 108 option.pre_validation({'d': 'value'}, 'd') 109 self.assertEqual(len(option.warnings), 1) 110 self.assertEqual(option.warnings[0], msg.format('d')) 111 112 def test_deprecated_option_with_type(self): 113 option = config_options.Deprecated(option_type=config_options.Type(str)) 114 option.pre_validation({'d': 'value'}, 'd') 115 self.assertEqual(len(option.warnings), 1) 116 option.validate('value') 117 118 def test_deprecated_option_with_invalid_type(self): 119 option = config_options.Deprecated(option_type=config_options.Type(list)) 120 config = {'d': 'string'} 121 option.pre_validation({'d': 'value'}, 'd') 122 self.assertEqual(len(option.warnings), 1) 123 self.assertRaises( 124 config_options.ValidationError, 125 option.validate, 126 config['d'] 127 ) 128 129 def test_deprecated_option_with_type_undefined(self): 130 option = config_options.Deprecated(option_type=config_options.Type(str)) 131 option.validate(None) 132 133 def test_deprecated_option_move(self): 134 option = config_options.Deprecated(moved_to='new') 135 config = {'old': 'value'} 136 option.pre_validation(config, 'old') 137 self.assertEqual(len(option.warnings), 1) 138 self.assertEqual(config, {'new': 'value'}) 139 140 def test_deprecated_option_move_complex(self): 141 option = config_options.Deprecated(moved_to='foo.bar') 142 config = {'old': 'value'} 143 option.pre_validation(config, 'old') 144 self.assertEqual(len(option.warnings), 1) 145 self.assertEqual(config, {'foo': {'bar': 'value'}}) 146 147 def test_deprecated_option_move_existing(self): 148 option = config_options.Deprecated(moved_to='foo.bar') 149 config = {'old': 'value', 'foo': {'existing': 'existing'}} 150 option.pre_validation(config, 'old') 151 self.assertEqual(len(option.warnings), 1) 152 self.assertEqual(config, {'foo': {'existing': 'existing', 'bar': 'value'}}) 153 154 def test_deprecated_option_move_invalid(self): 155 option = config_options.Deprecated(moved_to='foo.bar') 156 config = {'old': 'value', 'foo': 'wrong type'} 157 option.pre_validation(config, 'old') 158 self.assertEqual(len(option.warnings), 1) 159 self.assertEqual(config, {'old': 'value', 'foo': 'wrong type'}) 160 161 162class IpAddressTest(unittest.TestCase): 163 164 def test_valid_address(self): 165 addr = '127.0.0.1:8000' 166 167 option = config_options.IpAddress() 168 value = option.validate(addr) 169 self.assertEqual(str(value), addr) 170 self.assertEqual(value.host, '127.0.0.1') 171 self.assertEqual(value.port, 8000) 172 173 def test_valid_IPv6_address(self): 174 addr = '::1:8000' 175 176 option = config_options.IpAddress() 177 value = option.validate(addr) 178 self.assertEqual(str(value), addr) 179 self.assertEqual(value.host, '::1') 180 self.assertEqual(value.port, 8000) 181 182 def test_named_address(self): 183 addr = 'localhost:8000' 184 185 option = config_options.IpAddress() 186 value = option.validate(addr) 187 self.assertEqual(str(value), addr) 188 self.assertEqual(value.host, 'localhost') 189 self.assertEqual(value.port, 8000) 190 191 def test_default_address(self): 192 addr = '127.0.0.1:8000' 193 194 option = config_options.IpAddress(default=addr) 195 value = option.validate(None) 196 self.assertEqual(str(value), addr) 197 self.assertEqual(value.host, '127.0.0.1') 198 self.assertEqual(value.port, 8000) 199 200 @unittest.skipIf( 201 sys.version_info < (3, 9, 5), 202 "Leading zeros allowed in IP addresses before Python3.9.5", 203 ) 204 def test_invalid_leading_zeros(self): 205 addr = '127.000.000.001:8000' 206 option = config_options.IpAddress(default=addr) 207 self.assertRaises( 208 config_options.ValidationError, 209 option.validate, addr 210 ) 211 212 def test_invalid_address_range(self): 213 option = config_options.IpAddress() 214 self.assertRaises( 215 config_options.ValidationError, 216 option.validate, '277.0.0.1:8000' 217 ) 218 219 def test_invalid_address_format(self): 220 option = config_options.IpAddress() 221 self.assertRaises( 222 config_options.ValidationError, 223 option.validate, '127.0.0.18000' 224 ) 225 226 def test_invalid_address_type(self): 227 option = config_options.IpAddress() 228 self.assertRaises( 229 config_options.ValidationError, 230 option.validate, 123 231 ) 232 233 def test_invalid_address_port(self): 234 option = config_options.IpAddress() 235 self.assertRaises( 236 config_options.ValidationError, 237 option.validate, '127.0.0.1:foo' 238 ) 239 240 def test_invalid_address_missing_port(self): 241 option = config_options.IpAddress() 242 self.assertRaises( 243 config_options.ValidationError, 244 option.validate, '127.0.0.1' 245 ) 246 247 def test_unsupported_address(self): 248 option = config_options.IpAddress() 249 value = option.validate('0.0.0.0:8000') 250 option.post_validation({'dev_addr': value}, 'dev_addr') 251 self.assertEqual(len(option.warnings), 1) 252 253 def test_unsupported_IPv6_address(self): 254 option = config_options.IpAddress() 255 value = option.validate(':::8000') 256 option.post_validation({'dev_addr': value}, 'dev_addr') 257 self.assertEqual(len(option.warnings), 1) 258 259 def test_invalid_IPv6_address(self): 260 # The server will error out with this so we treat it as invalid. 261 option = config_options.IpAddress() 262 self.assertRaises( 263 config_options.ValidationError, 264 option.validate, '[::1]:8000' 265 ) 266 267 268class URLTest(unittest.TestCase): 269 270 def test_valid_url(self): 271 option = config_options.URL() 272 273 self.assertEqual(option.validate("https://mkdocs.org"), "https://mkdocs.org") 274 self.assertEqual(option.validate(""), "") 275 276 def test_valid_url_is_dir(self): 277 option = config_options.URL(is_dir=True) 278 279 self.assertEqual(option.validate("http://mkdocs.org/"), "http://mkdocs.org/") 280 self.assertEqual(option.validate("https://mkdocs.org"), "https://mkdocs.org/") 281 282 def test_invalid_url(self): 283 option = config_options.URL() 284 285 self.assertRaises(config_options.ValidationError, 286 option.validate, "www.mkdocs.org") 287 self.assertRaises(config_options.ValidationError, 288 option.validate, "//mkdocs.org/test") 289 self.assertRaises(config_options.ValidationError, 290 option.validate, "http:/mkdocs.org/") 291 self.assertRaises(config_options.ValidationError, 292 option.validate, "/hello/") 293 294 def test_invalid_type(self): 295 option = config_options.URL() 296 self.assertRaises(config_options.ValidationError, 297 option.validate, 1) 298 299 300class RepoURLTest(unittest.TestCase): 301 302 def test_repo_name_github(self): 303 304 option = config_options.RepoURL() 305 config = {'repo_url': "https://github.com/mkdocs/mkdocs"} 306 option.post_validation(config, 'repo_url') 307 self.assertEqual(config['repo_name'], "GitHub") 308 309 def test_repo_name_bitbucket(self): 310 311 option = config_options.RepoURL() 312 config = {'repo_url': "https://bitbucket.org/gutworth/six/"} 313 option.post_validation(config, 'repo_url') 314 self.assertEqual(config['repo_name'], "Bitbucket") 315 316 def test_repo_name_gitlab(self): 317 318 option = config_options.RepoURL() 319 config = {'repo_url': "https://gitlab.com/gitlab-org/gitlab-ce/"} 320 option.post_validation(config, 'repo_url') 321 self.assertEqual(config['repo_name'], "GitLab") 322 323 def test_repo_name_custom(self): 324 325 option = config_options.RepoURL() 326 config = {'repo_url': "https://launchpad.net/python-tuskarclient"} 327 option.post_validation(config, 'repo_url') 328 self.assertEqual(config['repo_name'], "Launchpad") 329 330 def test_edit_uri_github(self): 331 332 option = config_options.RepoURL() 333 config = {'repo_url': "https://github.com/mkdocs/mkdocs"} 334 option.post_validation(config, 'repo_url') 335 self.assertEqual(config['edit_uri'], 'edit/master/docs/') 336 337 def test_edit_uri_bitbucket(self): 338 339 option = config_options.RepoURL() 340 config = {'repo_url': "https://bitbucket.org/gutworth/six/"} 341 option.post_validation(config, 'repo_url') 342 self.assertEqual(config['edit_uri'], 'src/default/docs/') 343 344 def test_edit_uri_gitlab(self): 345 346 option = config_options.RepoURL() 347 config = {'repo_url': "https://gitlab.com/gitlab-org/gitlab-ce/"} 348 option.post_validation(config, 'repo_url') 349 self.assertEqual(config['edit_uri'], 'edit/master/docs/') 350 351 def test_edit_uri_custom(self): 352 353 option = config_options.RepoURL() 354 config = {'repo_url': "https://launchpad.net/python-tuskarclient"} 355 option.post_validation(config, 'repo_url') 356 self.assertEqual(config.get('edit_uri'), '') 357 358 def test_repo_name_custom_and_empty_edit_uri(self): 359 360 option = config_options.RepoURL() 361 config = {'repo_url': "https://github.com/mkdocs/mkdocs", 362 'repo_name': 'mkdocs'} 363 option.post_validation(config, 'repo_url') 364 self.assertEqual(config.get('edit_uri'), 'edit/master/docs/') 365 366 367class DirTest(unittest.TestCase): 368 369 def test_valid_dir(self): 370 371 d = os.path.dirname(__file__) 372 option = config_options.Dir(exists=True) 373 value = option.validate(d) 374 self.assertEqual(d, value) 375 376 def test_missing_dir(self): 377 378 d = os.path.join("not", "a", "real", "path", "I", "hope") 379 option = config_options.Dir() 380 value = option.validate(d) 381 self.assertEqual(os.path.abspath(d), value) 382 383 def test_missing_dir_but_required(self): 384 385 d = os.path.join("not", "a", "real", "path", "I", "hope") 386 option = config_options.Dir(exists=True) 387 self.assertRaises(config_options.ValidationError, 388 option.validate, d) 389 390 def test_file(self): 391 d = __file__ 392 option = config_options.Dir(exists=True) 393 self.assertRaises(config_options.ValidationError, 394 option.validate, d) 395 396 def test_incorrect_type_attribute_error(self): 397 option = config_options.Dir() 398 self.assertRaises(config_options.ValidationError, 399 option.validate, 1) 400 401 def test_incorrect_type_type_error(self): 402 option = config_options.Dir() 403 self.assertRaises(config_options.ValidationError, 404 option.validate, []) 405 406 def test_dir_unicode(self): 407 cfg = Config( 408 [('dir', config_options.Dir())], 409 config_file_path=os.path.join(os.path.abspath('.'), 'mkdocs.yml'), 410 ) 411 412 test_config = { 413 'dir': 'юникод' 414 } 415 416 cfg.load_dict(test_config) 417 418 fails, warns = cfg.validate() 419 420 self.assertEqual(len(fails), 0) 421 self.assertEqual(len(warns), 0) 422 self.assertIsInstance(cfg['dir'], str) 423 424 def test_dir_filesystemencoding(self): 425 cfg = Config( 426 [('dir', config_options.Dir())], 427 config_file_path=os.path.join(os.path.abspath('.'), 'mkdocs.yml'), 428 ) 429 430 test_config = { 431 'dir': 'Übersicht'.encode(encoding=sys.getfilesystemencoding()) 432 } 433 434 cfg.load_dict(test_config) 435 436 fails, warns = cfg.validate() 437 438 # str does not include byte strings so validation fails 439 self.assertEqual(len(fails), 1) 440 self.assertEqual(len(warns), 0) 441 442 def test_dir_bad_encoding_fails(self): 443 cfg = Config( 444 [('dir', config_options.Dir())], 445 config_file_path=os.path.join(os.path.abspath('.'), 'mkdocs.yml'), 446 ) 447 448 test_config = { 449 'dir': 'юникод'.encode(encoding='ISO 8859-5') 450 } 451 452 cfg.load_dict(test_config) 453 454 fails, warns = cfg.validate() 455 456 self.assertEqual(len(fails), 1) 457 self.assertEqual(len(warns), 0) 458 459 def test_config_dir_prepended(self): 460 base_path = os.path.abspath('.') 461 cfg = Config( 462 [('dir', config_options.Dir())], 463 config_file_path=os.path.join(base_path, 'mkdocs.yml'), 464 ) 465 466 test_config = { 467 'dir': 'foo' 468 } 469 470 cfg.load_dict(test_config) 471 472 fails, warns = cfg.validate() 473 474 self.assertEqual(len(fails), 0) 475 self.assertEqual(len(warns), 0) 476 self.assertIsInstance(cfg['dir'], str) 477 self.assertEqual(cfg['dir'], os.path.join(base_path, 'foo')) 478 479 def test_dir_is_config_dir_fails(self): 480 cfg = Config( 481 [('dir', config_options.Dir())], 482 config_file_path=os.path.join(os.path.abspath('.'), 'mkdocs.yml'), 483 ) 484 485 test_config = { 486 'dir': '.' 487 } 488 489 cfg.load_dict(test_config) 490 491 fails, warns = cfg.validate() 492 493 self.assertEqual(len(fails), 1) 494 self.assertEqual(len(warns), 0) 495 496 497class SiteDirTest(unittest.TestCase): 498 499 def validate_config(self, config): 500 """ Given a config with values for site_dir and doc_dir, run site_dir post_validation. """ 501 site_dir = config_options.SiteDir() 502 docs_dir = config_options.Dir() 503 504 fname = os.path.join(os.path.abspath('..'), 'mkdocs.yml') 505 506 config['docs_dir'] = docs_dir.validate(config['docs_dir']) 507 config['site_dir'] = site_dir.validate(config['site_dir']) 508 509 schema = [ 510 ('site_dir', site_dir), 511 ('docs_dir', docs_dir), 512 ] 513 cfg = Config(schema, fname) 514 cfg.load_dict(config) 515 failed, warned = cfg.validate() 516 517 if failed: 518 raise config_options.ValidationError(failed) 519 520 return True 521 522 def test_doc_dir_in_site_dir(self): 523 524 j = os.path.join 525 # The parent dir is not the same on every system, so use the actual dir name 526 parent_dir = mkdocs.__file__.split(os.sep)[-3] 527 528 test_configs = ( 529 {'docs_dir': j('site', 'docs'), 'site_dir': 'site'}, 530 {'docs_dir': 'docs', 'site_dir': '.'}, 531 {'docs_dir': '.', 'site_dir': '.'}, 532 {'docs_dir': 'docs', 'site_dir': ''}, 533 {'docs_dir': '', 'site_dir': ''}, 534 {'docs_dir': j('..', parent_dir, 'docs'), 'site_dir': 'docs'}, 535 {'docs_dir': 'docs', 'site_dir': '/'} 536 ) 537 538 for test_config in test_configs: 539 self.assertRaises(config_options.ValidationError, 540 self.validate_config, test_config) 541 542 def test_site_dir_in_docs_dir(self): 543 544 j = os.path.join 545 546 test_configs = ( 547 {'docs_dir': 'docs', 'site_dir': j('docs', 'site')}, 548 {'docs_dir': '.', 'site_dir': 'site'}, 549 {'docs_dir': '', 'site_dir': 'site'}, 550 {'docs_dir': '/', 'site_dir': 'site'}, 551 ) 552 553 for test_config in test_configs: 554 self.assertRaises(config_options.ValidationError, 555 self.validate_config, test_config) 556 557 def test_common_prefix(self): 558 """ Legitimate settings with common prefixes should not fail validation. """ 559 560 test_configs = ( 561 {'docs_dir': 'docs', 'site_dir': 'docs-site'}, 562 {'docs_dir': 'site-docs', 'site_dir': 'site'}, 563 ) 564 565 for test_config in test_configs: 566 assert self.validate_config(test_config) 567 568 569class ThemeTest(unittest.TestCase): 570 571 def test_theme_as_string(self): 572 573 option = config_options.Theme() 574 value = option.validate("mkdocs") 575 self.assertEqual({'name': 'mkdocs'}, value) 576 577 def test_uninstalled_theme_as_string(self): 578 579 option = config_options.Theme() 580 self.assertRaises(config_options.ValidationError, 581 option.validate, "mkdocs2") 582 583 def test_theme_default(self): 584 option = config_options.Theme(default='mkdocs') 585 value = option.validate(None) 586 self.assertEqual({'name': 'mkdocs'}, value) 587 588 def test_theme_as_simple_config(self): 589 590 config = { 591 'name': 'mkdocs' 592 } 593 option = config_options.Theme() 594 value = option.validate(config) 595 self.assertEqual(config, value) 596 597 def test_theme_as_complex_config(self): 598 599 config = { 600 'name': 'mkdocs', 601 'custom_dir': 'custom', 602 'static_templates': ['sitemap.html'], 603 'show_sidebar': False 604 } 605 option = config_options.Theme() 606 value = option.validate(config) 607 self.assertEqual(config, value) 608 609 def test_theme_name_is_none(self): 610 611 config = { 612 'name': None 613 } 614 option = config_options.Theme() 615 value = option.validate(config) 616 self.assertEqual(config, value) 617 618 def test_theme_config_missing_name(self): 619 620 config = { 621 'custom_dir': 'custom', 622 } 623 option = config_options.Theme() 624 self.assertRaises(config_options.ValidationError, 625 option.validate, config) 626 627 def test_uninstalled_theme_as_config(self): 628 629 config = { 630 'name': 'mkdocs2' 631 } 632 option = config_options.Theme() 633 self.assertRaises(config_options.ValidationError, 634 option.validate, config) 635 636 def test_theme_invalid_type(self): 637 638 config = ['mkdocs2'] 639 option = config_options.Theme() 640 self.assertRaises(config_options.ValidationError, 641 option.validate, config) 642 643 def test_post_validation_none_theme_name_and_missing_custom_dir(self): 644 645 config = { 646 'theme': { 647 'name': None 648 } 649 } 650 option = config_options.Theme() 651 self.assertRaises(config_options.ValidationError, 652 option.post_validation, config, 'theme') 653 654 @tempdir() 655 def test_post_validation_inexisting_custom_dir(self, abs_base_path): 656 657 config = { 658 'theme': { 659 'name': None, 660 'custom_dir': abs_base_path + '/inexisting_custom_dir', 661 } 662 } 663 option = config_options.Theme() 664 self.assertRaises(config_options.ValidationError, 665 option.post_validation, config, 'theme') 666 667 def test_post_validation_locale_none(self): 668 669 config = { 670 'theme': { 671 'name': 'mkdocs', 672 'locale': None 673 } 674 } 675 option = config_options.Theme() 676 self.assertRaises(config_options.ValidationError, 677 option.post_validation, config, 'theme') 678 679 def test_post_validation_locale_invalid_type(self): 680 681 config = { 682 'theme': { 683 'name': 'mkdocs', 684 'locale': 0 685 } 686 } 687 option = config_options.Theme() 688 self.assertRaises(config_options.ValidationError, 689 option.post_validation, config, 'theme') 690 691 def test_post_validation_locale(self): 692 693 config = { 694 'theme': { 695 'name': 'mkdocs', 696 'locale': 'fr' 697 } 698 } 699 option = config_options.Theme() 700 option.post_validation(config, 'theme') 701 self.assertEqual('fr', config['theme']['locale'].language) 702 703 704class NavTest(unittest.TestCase): 705 706 def test_old_format(self): 707 708 option = config_options.Nav() 709 self.assertRaises( 710 config_options.ValidationError, 711 option.validate, 712 [['index.md', ], ] 713 ) 714 715 def test_provided_dict(self): 716 717 option = config_options.Nav() 718 value = option.validate([ 719 'index.md', 720 {"Page": "page.md"} 721 ]) 722 self.assertEqual(['index.md', {'Page': 'page.md'}], value) 723 724 option.post_validation({'extra_stuff': []}, 'extra_stuff') 725 726 def test_provided_empty(self): 727 728 option = config_options.Nav() 729 value = option.validate([]) 730 self.assertEqual(None, value) 731 732 option.post_validation({'extra_stuff': []}, 'extra_stuff') 733 734 def test_invalid_type(self): 735 736 option = config_options.Nav() 737 self.assertRaises(config_options.ValidationError, 738 option.validate, {}) 739 740 def test_invalid_config(self): 741 742 option = config_options.Nav() 743 self.assertRaises(config_options.ValidationError, 744 option.validate, [[], 1]) 745 746 747class PrivateTest(unittest.TestCase): 748 749 def test_defined(self): 750 751 option = config_options.Private() 752 self.assertRaises(config_options.ValidationError, 753 option.validate, 'somevalue') 754 755 756class MarkdownExtensionsTest(unittest.TestCase): 757 758 @patch('markdown.Markdown') 759 def test_simple_list(self, mockMd): 760 option = config_options.MarkdownExtensions() 761 config = { 762 'markdown_extensions': ['foo', 'bar'] 763 } 764 config['markdown_extensions'] = option.validate(config['markdown_extensions']) 765 option.post_validation(config, 'markdown_extensions') 766 self.assertEqual({ 767 'markdown_extensions': ['foo', 'bar'], 768 'mdx_configs': {} 769 }, config) 770 771 @patch('markdown.Markdown') 772 def test_list_dicts(self, mockMd): 773 option = config_options.MarkdownExtensions() 774 config = { 775 'markdown_extensions': [ 776 {'foo': {'foo_option': 'foo value'}}, 777 {'bar': {'bar_option': 'bar value'}}, 778 {'baz': None} 779 ] 780 } 781 config['markdown_extensions'] = option.validate(config['markdown_extensions']) 782 option.post_validation(config, 'markdown_extensions') 783 self.assertEqual({ 784 'markdown_extensions': ['foo', 'bar', 'baz'], 785 'mdx_configs': { 786 'foo': {'foo_option': 'foo value'}, 787 'bar': {'bar_option': 'bar value'} 788 } 789 }, config) 790 791 @patch('markdown.Markdown') 792 def test_mixed_list(self, mockMd): 793 option = config_options.MarkdownExtensions() 794 config = { 795 'markdown_extensions': [ 796 'foo', 797 {'bar': {'bar_option': 'bar value'}} 798 ] 799 } 800 config['markdown_extensions'] = option.validate(config['markdown_extensions']) 801 option.post_validation(config, 'markdown_extensions') 802 self.assertEqual({ 803 'markdown_extensions': ['foo', 'bar'], 804 'mdx_configs': { 805 'bar': {'bar_option': 'bar value'} 806 } 807 }, config) 808 809 @patch('markdown.Markdown') 810 def test_dict_of_dicts(self, mockMd): 811 option = config_options.MarkdownExtensions() 812 config = { 813 'markdown_extensions': { 814 'foo': {'foo_option': 'foo value'}, 815 'bar': {'bar_option': 'bar value'}, 816 'baz': {} 817 } 818 } 819 config['markdown_extensions'] = option.validate(config['markdown_extensions']) 820 option.post_validation(config, 'markdown_extensions') 821 self.assertEqual({ 822 'markdown_extensions': ['foo', 'bar', 'baz'], 823 'mdx_configs': { 824 'foo': {'foo_option': 'foo value'}, 825 'bar': {'bar_option': 'bar value'} 826 } 827 }, config) 828 829 @patch('markdown.Markdown') 830 def test_builtins(self, mockMd): 831 option = config_options.MarkdownExtensions(builtins=['meta', 'toc']) 832 config = { 833 'markdown_extensions': ['foo', 'bar'] 834 } 835 config['markdown_extensions'] = option.validate(config['markdown_extensions']) 836 option.post_validation(config, 'markdown_extensions') 837 self.assertEqual({ 838 'markdown_extensions': ['meta', 'toc', 'foo', 'bar'], 839 'mdx_configs': {} 840 }, config) 841 842 def test_duplicates(self): 843 option = config_options.MarkdownExtensions(builtins=['meta', 'toc']) 844 config = { 845 'markdown_extensions': ['meta', 'toc'] 846 } 847 config['markdown_extensions'] = option.validate(config['markdown_extensions']) 848 option.post_validation(config, 'markdown_extensions') 849 self.assertEqual({ 850 'markdown_extensions': ['meta', 'toc'], 851 'mdx_configs': {} 852 }, config) 853 854 def test_builtins_config(self): 855 option = config_options.MarkdownExtensions(builtins=['meta', 'toc']) 856 config = { 857 'markdown_extensions': [ 858 {'toc': {'permalink': True}} 859 ] 860 } 861 config['markdown_extensions'] = option.validate(config['markdown_extensions']) 862 option.post_validation(config, 'markdown_extensions') 863 self.assertEqual({ 864 'markdown_extensions': ['meta', 'toc'], 865 'mdx_configs': {'toc': {'permalink': True}} 866 }, config) 867 868 @patch('markdown.Markdown') 869 def test_configkey(self, mockMd): 870 option = config_options.MarkdownExtensions(configkey='bar') 871 config = { 872 'markdown_extensions': [ 873 {'foo': {'foo_option': 'foo value'}} 874 ] 875 } 876 config['markdown_extensions'] = option.validate(config['markdown_extensions']) 877 option.post_validation(config, 'markdown_extensions') 878 self.assertEqual({ 879 'markdown_extensions': ['foo'], 880 'bar': { 881 'foo': {'foo_option': 'foo value'} 882 } 883 }, config) 884 885 def test_none(self): 886 option = config_options.MarkdownExtensions(default=[]) 887 config = { 888 'markdown_extensions': None 889 } 890 config['markdown_extensions'] = option.validate(config['markdown_extensions']) 891 option.post_validation(config, 'markdown_extensions') 892 self.assertEqual({ 893 'markdown_extensions': [], 894 'mdx_configs': {} 895 }, config) 896 897 @patch('markdown.Markdown') 898 def test_not_list(self, mockMd): 899 option = config_options.MarkdownExtensions() 900 self.assertRaises(config_options.ValidationError, 901 option.validate, 'not a list') 902 903 @patch('markdown.Markdown') 904 def test_invalid_config_option(self, mockMd): 905 option = config_options.MarkdownExtensions() 906 config = { 907 'markdown_extensions': [ 908 {'foo': 'not a dict'} 909 ] 910 } 911 self.assertRaises( 912 config_options.ValidationError, 913 option.validate, config['markdown_extensions'] 914 ) 915 916 @patch('markdown.Markdown') 917 def test_invalid_config_item(self, mockMd): 918 option = config_options.MarkdownExtensions() 919 config = { 920 'markdown_extensions': [ 921 ['not a dict'] 922 ] 923 } 924 self.assertRaises( 925 config_options.ValidationError, 926 option.validate, config['markdown_extensions'] 927 ) 928 929 @patch('markdown.Markdown') 930 def test_invalid_dict_item(self, mockMd): 931 option = config_options.MarkdownExtensions() 932 config = { 933 'markdown_extensions': [ 934 {'key1': 'value', 'key2': 'too many keys'} 935 ] 936 } 937 self.assertRaises( 938 config_options.ValidationError, 939 option.validate, config['markdown_extensions'] 940 ) 941 942 def test_unknown_extension(self): 943 option = config_options.MarkdownExtensions() 944 config = { 945 'markdown_extensions': ['unknown'] 946 } 947 self.assertRaises( 948 config_options.ValidationError, 949 option.validate, config['markdown_extensions'] 950 ) 951