#!/usr/bin/env python3 # $Id$ # Author: David Goodger # Copyright: This module has been placed in the public domain. """ Tests of runtime settings. """ import sys import os import difflib import pprint import warnings import unittest from docutils import frontend, utils from docutils.writers import pep_html, html5_polyglot from docutils.parsers import rst def fixpath(path): return os.path.abspath(os.path.join(*(path.split('/')))) class ConfigFileTests(unittest.TestCase): config_files = {'old': fixpath('data/config_old.txt'), 'one': fixpath('data/config_1.txt'), 'two': fixpath('data/config_2.txt'), 'list': fixpath('data/config_list.txt'), 'list2': fixpath('data/config_list_2.txt'), 'error': fixpath('data/config_encoding.txt'), 'error2': fixpath('data/config_encoding_2.txt'), 'syntax_error': fixpath('data/config_syntax_error.txt'), } # expected settings after parsing the equally named config_file: settings = { 'old': {'datestamp': '%Y-%m-%d %H:%M UTC', 'generator': True, 'no_random': True, 'python_home': 'http://www.python.org', 'source_link': True, 'stylesheet': None, 'stylesheet_path': ['stylesheets/pep.css'], 'template': fixpath('data/pep-html-template'), }, 'one': {'datestamp': '%Y-%m-%d %H:%M UTC', 'generator': True, 'no_random': True, 'python_home': 'http://www.python.org', 'raw_enabled': False, 'record_dependencies': utils.DependencyList(), 'source_link': True, 'stylesheet': None, 'stylesheet_path': ['stylesheets/pep.css'], 'tab_width': 8, 'template': fixpath('data/pep-html-template'), 'trim_footnote_reference_space': True, 'output_encoding': 'ascii', 'output_encoding_error_handler': 'xmlcharrefreplace', }, 'two': {'footnote_references': 'superscript', 'generator': False, 'record_dependencies': utils.DependencyList(), 'stylesheet': None, 'stylesheet_path': ['test.css'], 'trim_footnote_reference_space': None, 'output_encoding_error_handler': 'namereplace', }, 'two (html5)': { # use defaults from html5_polyglot writer component # ignore settings in [html4css1 writer] section, 'generator': True, 'raw_enabled': False, 'record_dependencies': utils.DependencyList(), 'source_link': False, 'tab_width': 8, 'trim_footnote_reference_space': True, 'output_encoding_error_handler': 'namereplace', }, 'list': {'expose_internals': ['a', 'b', 'c', 'd', 'e'], 'smartquotes_locales': [('de', '«»‹›')], 'strip_classes': ['spam', 'pan', 'fun', 'parrot'], 'strip_elements_with_classes': ['sugar', 'flour', 'milk', 'safran'] }, 'list2': {'expose_internals': ['a', 'b', 'c', 'd', 'e', 'f'], 'smartquotes_locales': [('de', '«»‹›'), ('nl', '„”’’'), ('cs', '»«›‹'), ('fr', ['« ', ' »', '‹ ', ' ›']) ], 'strip_classes': ['spam', 'pan', 'fun', 'parrot', 'ham', 'eggs'], 'strip_elements_with_classes': ['sugar', 'flour', 'milk', 'safran', 'eggs', 'salt'], 'stylesheet': ['style2.css', 'style3.css'], 'stylesheet_path': None, }, 'error': {'error_encoding': 'ascii', 'error_encoding_error_handler': 'strict'}, 'error2': {'error_encoding': 'latin1'}, } compare = difflib.Differ().compare """Comparison method shared by all tests.""" def setUp(self): warnings.filterwarnings('ignore', category=frontend.ConfigDeprecationWarning) warnings.filterwarnings('ignore', category=DeprecationWarning) self.option_parser = frontend.OptionParser( components=(pep_html.Writer, rst.Parser), read_config_files=None) def files_settings(self, *names): settings = frontend.Values() for name in names: cfs = self.option_parser.get_config_file_settings( self.config_files[name]) settings.update(cfs, self.option_parser) return settings.__dict__ def expected_settings(self, *names): expected = {} for name in names: expected.update(self.settings[name]) return expected def compare_output(self, result, expected): """`result` and `expected` should both be dicts.""" self.assertTrue('record_dependencies' in result) if 'record_dependencies' not in expected: # Delete it if we don't want to test it. del result['record_dependencies'] result = pprint.pformat(result) + '\n' expected = pprint.pformat(expected) + '\n' try: self.assertEqual(result, expected) except AssertionError: print('\n%s\n' % (self,), file=sys.stderr) print('-: expected\n+: result', file=sys.stderr) print(''.join(self.compare( expected.splitlines(True), result.splitlines(True))), file=sys.stderr) raise def test_nofiles(self): self.compare_output(self.files_settings(), self.expected_settings()) def test_old(self): with self.assertWarnsRegex(FutureWarning, r'The "\[option\]" section is deprecated.'): self.files_settings('old') def test_syntax_error(self): with self.assertRaisesRegex( ValueError, 'Error in config file ".*config_syntax_error.txt", ' r'section "\[general\]"'): self.files_settings('syntax_error') def test_one(self): self.compare_output(self.files_settings('one'), self.expected_settings('one')) def test_multiple(self): self.compare_output(self.files_settings('one', 'two'), self.expected_settings('one', 'two')) def test_multiple_with_html5_writer(self): # initialize option parser with different component set self.option_parser = frontend.OptionParser( components=(html5_polyglot.Writer, rst.Parser), read_config_files=None) # generator setting not changed by "config_2.txt": self.compare_output(self.files_settings('one', 'two'), self.expected_settings('two (html5)')) def test_old_and_new(self): self.compare_output(self.files_settings('old', 'two'), self.expected_settings('old', 'two')) def test_list(self): self.compare_output(self.files_settings('list'), self.expected_settings('list')) def test_list2(self): # setting `stylesheet` in 'list2' resets stylesheet_path to None self.compare_output(self.files_settings('list', 'list2'), self.expected_settings('list2')) def test_encoding_error_handler(self): # set error_encoding and error_encoding_error_handler (from affix) self.compare_output(self.files_settings('error'), self.expected_settings('error')) def test_encoding_error_handler2(self): # second config file only changes encoding, not error_handler: self.compare_output(self.files_settings('error', 'error2'), self.expected_settings('error', 'error2')) class ConfigEnvVarFileTests(ConfigFileTests): """ Repeats the tests of `ConfigFileTests` using the ``DOCUTILSCONFIG`` environment variable and the standard Docutils config file mechanism. """ def setUp(self): ConfigFileTests.setUp(self) self.orig_environ = os.environ os.environ = os.environ.copy() def files_settings(self, *names): files = [self.config_files[name] for name in names] os.environ['DOCUTILSCONFIG'] = os.pathsep.join(files) settings = self.option_parser.get_standard_config_settings() return settings.__dict__ def tearDown(self): os.environ = self.orig_environ def test_old(self): pass # don't repreat this test @unittest.skipUnless( os.name == 'posix', 'os.path.expanduser() does not use HOME on Windows (since 3.8)') def test_get_standard_config_files(self): os.environ['HOME'] = '/home/parrot' # TODO: set up mock home directory under Windows self.assertEqual(self.option_parser.get_standard_config_files(), ['/etc/docutils.conf', './docutils.conf', '/home/parrot/.docutils']) # split at ':', expand leading '~': os.environ['DOCUTILSCONFIG'] = ('/etc/docutils2.conf' ':~/.config/docutils.conf') self.assertEqual(self.option_parser.get_standard_config_files(), ['/etc/docutils2.conf', '/home/parrot/.config/docutils.conf']) class HelperFunctionsTests(unittest.TestCase): pathdict = {'foo': 'hallo', 'ham': 'h\xE4m', 'spam': 'spam'} keys = ['foo', 'ham'] def setUp(self): self.option_parser = frontend.OptionParser( components=(rst.Parser,), read_config_files=None) def test_make_paths_absolute(self): pathdict = self.pathdict.copy() frontend.make_paths_absolute(pathdict, self.keys, base_path='base') self.assertEqual(pathdict['foo'], os.path.abspath('base/hallo')) self.assertEqual(pathdict['ham'], os.path.abspath('base/h\xE4m')) # not touched, because key not in keys: self.assertEqual(pathdict['spam'], 'spam') def test_make_paths_absolute_cwd(self): # With base_path None, the cwd is used as base path. # Settings values may-be `unicode` instances, therefore # os.getcwdu() is used and the converted path is a unicode instance: pathdict = self.pathdict.copy() frontend.make_paths_absolute(pathdict, self.keys) self.assertEqual(pathdict['foo'], os.path.abspath('hallo')) self.assertEqual(pathdict['ham'], os.path.abspath('h\xE4m')) # not touched, because key not in keys: self.assertEqual(pathdict['spam'], 'spam') boolean_settings = ( (True, True), ('1', True), ('on', True), ('yes', True), ('true', True), ('0', False), ('off', False), ('no', False), ('false', False), ) def test_validate_boolean(self): for t in self.boolean_settings: self.assertEqual( frontend.validate_boolean(None, t[0], self.option_parser), t[1]) def test_validate_ternary(self): tests = ( ('500V', '500V'), ('parrot', 'parrot'), ) for t in self.boolean_settings + tests: self.assertEqual( frontend.validate_ternary(None, t[0], self.option_parser), t[1]) def test_validate_colon_separated_string_list(self): tests = ( ('a', ['a']), ('a:b', ['a', 'b']), (['a'], ['a']), (['a', 'b:c'], ['a', 'b', 'c']), ) for t in tests: self.assertEqual(frontend.validate_colon_separated_string_list( None, t[0], None), t[1]) def test_validate_comma_separated_list(self): tests = ( ('a', ['a']), ('a,b', ['a', 'b']), (['a'], ['a']), (['a', 'b,c'], ['a', 'b', 'c']), ) for t in tests: self.assertEqual( frontend.validate_comma_separated_list(None, t[0], None), t[1]) def test_validate_url_trailing_slash(self): tests = ( ('', './'), (None, './'), ('http://example.org', 'http://example.org/'), ('http://example.org/', 'http://example.org/'), ) for t in tests: self.assertEqual( frontend.validate_url_trailing_slash(None, t[0], None), t[1]) def test_validate_smartquotes_locales(self): tests = ( ('en:ssvv', [('en', 'ssvv')]), ('sd:«»°°', [('sd', '«»°°')]), ([('sd', '«»°°'), 'ds:°°«»'], [('sd', '«»°°'), ('ds', '°°«»')]), ('frs:« : »:((:))', [('frs', ['« ', ' »', '((', '))'])]), ) for t in tests: self.assertEqual( frontend.validate_smartquotes_locales(None, t[0], None), t[1]) def test_set_conditions_deprecation_warning(self): reporter = utils.Reporter('test', 1, 4) with self.assertWarnsRegex(DeprecationWarning, 'Set attributes via configuration '): reporter.set_conditions('foo', 1, 4) # trigger warning if __name__ == '__main__': unittest.main()