Mask trailing whitespace in test sample.
[docutils.git] / docutils / test / test_settings.py
blob3e31cb32c6d5f69d2420d448788a2f1bc72d9d13
1 #!/usr/bin/env python3
2 # $Id$
3 # Author: David Goodger <goodger@python.org>
4 # Copyright: This module has been placed in the public domain.
6 """
7 Tests of runtime settings.
8 """
10 import os
11 import difflib
12 import warnings
13 from pathlib import Path
14 import sys
15 import unittest
17 if __name__ == '__main__':
18 # prepend the "docutils root" to the Python library path
19 # so we import the local `docutils` package.
20 sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
22 from docutils import frontend, utils
23 from docutils.writers import pep_html, html5_polyglot
24 from docutils.parsers import rst
26 # DATA_ROOT is ./test/data/ from the docutils root
27 DATA_ROOT = os.path.abspath(os.path.join(__file__, '..', 'data'))
30 def fixpath(path):
31 return os.path.join(DATA_ROOT, path)
34 class ConfigFileTests(unittest.TestCase):
36 config_files = {'old': fixpath('config_old.txt'),
37 'one': fixpath('config_1.txt'),
38 'two': fixpath('config_2.txt'),
39 'list': fixpath('config_list.txt'),
40 'list2': fixpath('config_list_2.txt'),
41 'error': fixpath('config_encoding.txt'),
42 'error2': fixpath('config_encoding_2.txt'),
43 'syntax_error': fixpath('config_syntax_error.txt'),
46 # expected settings after parsing the equally named config_file:
47 settings = {
48 'old': {'datestamp': '%Y-%m-%d %H:%M UTC',
49 'generator': True,
50 'no_random': True,
51 'python_home': 'http://www.python.org',
52 'source_link': True,
53 'stylesheet': None,
54 'stylesheet_path': ['stylesheets/pep.css'],
55 'template': fixpath('pep-html-template'),
57 'one': {'datestamp': '%Y-%m-%d %H:%M UTC',
58 'generator': True,
59 'no_random': True,
60 'python_home': 'http://www.python.org',
61 'raw_enabled': False,
62 'record_dependencies': utils.DependencyList(),
63 'source_link': True,
64 'stylesheet': None,
65 'stylesheet_path': ['stylesheets/pep.css'],
66 'tab_width': 8,
67 'template': fixpath('pep-html-template'),
68 'trim_footnote_reference_space': True,
69 'output_encoding': 'ascii',
70 'output_encoding_error_handler': 'xmlcharrefreplace',
72 'two': {'footnote_references': 'superscript',
73 'generator': False,
74 'record_dependencies': utils.DependencyList(),
75 'stylesheet': None,
76 'stylesheet_path': ['test.css'],
77 'trim_footnote_reference_space': None,
78 'output_encoding_error_handler': 'namereplace',
80 'two (html5)': {
81 # use defaults from html5_polyglot writer component
82 # ignore settings in [html4css1 writer] section,
83 'generator': True,
84 'raw_enabled': False,
85 'record_dependencies': utils.DependencyList(),
86 'source_link': False,
87 'tab_width': 8,
88 'trim_footnote_reference_space': True,
89 'output_encoding_error_handler': 'namereplace',
91 'list': {'expose_internals': ['a', 'b', 'c', 'd', 'e'],
92 'smartquotes_locales': [('de', '«»‹›')],
93 'strip_classes': ['spam', 'pan', 'fun', 'parrot'],
94 'strip_elements_with_classes': ['sugar', 'flour', 'milk',
95 'safran']
97 'list2': {'expose_internals': ['a', 'b', 'c', 'd', 'e', 'f'],
98 'smartquotes_locales': [('de', '«»‹›'),
99 ('nl', '„”’’'),
100 ('cs', '»«›‹'),
101 ('fr', ['« ', ' »', '‹ ', ' ›'])
103 'strip_classes': ['spam', 'pan', 'fun', 'parrot',
104 'ham', 'eggs'],
105 'strip_elements_with_classes': ['sugar', 'flour', 'milk',
106 'safran', 'eggs', 'salt'],
107 'stylesheet': ['style2.css', 'style3.css'],
108 'stylesheet_path': None,
110 'error': {'error_encoding': 'ascii',
111 'error_encoding_error_handler': 'strict'},
112 'error2': {'error_encoding': 'latin1'},
115 compare = difflib.Differ().compare
116 """Comparison method shared by all tests."""
118 def setUp(self):
119 warnings.filterwarnings('ignore',
120 category=frontend.ConfigDeprecationWarning)
121 warnings.filterwarnings('ignore', category=DeprecationWarning)
122 self.option_parser = frontend.OptionParser(
123 components=(pep_html.Writer, rst.Parser), read_config_files=None)
125 def files_settings(self, *names):
126 settings = frontend.Values()
127 for name in names:
128 cfs = self.option_parser.get_config_file_settings(
129 self.config_files[name])
130 settings.update(cfs, self.option_parser)
131 return settings.__dict__
133 def expected_settings(self, *names):
134 expected = {}
135 for name in names:
136 expected.update(self.settings[name])
137 return expected
139 def compare_output(self, result, expected):
140 """`result` and `expected` should both be dicts."""
141 self.assertIn('record_dependencies', result)
142 rd_result = result.pop('record_dependencies')
143 rd_expected = expected.pop('record_dependencies', None)
144 if rd_expected is not None:
145 self.assertEqual(str(rd_result), str(rd_expected))
146 self.assertEqual(expected, result)
148 def test_nofiles(self):
149 self.compare_output(self.files_settings(),
150 self.expected_settings())
152 def test_old(self):
153 with self.assertWarnsRegex(FutureWarning,
154 r'The "\[option\]" section is deprecated.'):
155 self.files_settings('old')
157 def test_syntax_error(self):
158 with self.assertRaisesRegex(
159 ValueError,
160 'Error in config file ".*config_syntax_error.txt", '
161 r'section "\[general\]"'):
162 self.files_settings('syntax_error')
164 def test_one(self):
165 self.compare_output(self.files_settings('one'),
166 self.expected_settings('one'))
168 def test_multiple(self):
169 self.compare_output(self.files_settings('one', 'two'),
170 self.expected_settings('one', 'two'))
172 def test_multiple_with_html5_writer(self):
173 # initialize option parser with different component set
174 self.option_parser = frontend.OptionParser(
175 components=(html5_polyglot.Writer, rst.Parser),
176 read_config_files=None)
177 # generator setting not changed by "config_2.txt":
178 self.compare_output(self.files_settings('one', 'two'),
179 self.expected_settings('two (html5)'))
181 def test_old_and_new(self):
182 self.compare_output(self.files_settings('old', 'two'),
183 self.expected_settings('old', 'two'))
185 def test_list(self):
186 self.compare_output(self.files_settings('list'),
187 self.expected_settings('list'))
189 def test_list2(self):
190 # setting `stylesheet` in 'list2' resets stylesheet_path to None
191 self.compare_output(self.files_settings('list', 'list2'),
192 self.expected_settings('list2'))
194 def test_encoding_error_handler(self):
195 # set error_encoding and error_encoding_error_handler (from affix)
196 self.compare_output(self.files_settings('error'),
197 self.expected_settings('error'))
199 def test_encoding_error_handler2(self):
200 # second config file only changes encoding, not error_handler:
201 self.compare_output(self.files_settings('error', 'error2'),
202 self.expected_settings('error', 'error2'))
205 class ConfigEnvVarFileTests(ConfigFileTests):
208 Repeats the tests of `ConfigFileTests` using the ``DOCUTILSCONFIG``
209 environment variable and the standard Docutils config file mechanism.
212 def setUp(self):
213 ConfigFileTests.setUp(self)
214 self.orig_environ = os.environ
215 os.environ = os.environ.copy()
217 def files_settings(self, *names):
218 files = [self.config_files[name] for name in names]
219 os.environ['DOCUTILSCONFIG'] = os.pathsep.join(files)
220 settings = self.option_parser.get_standard_config_settings()
221 return settings.__dict__
223 def tearDown(self):
224 os.environ = self.orig_environ
226 def test_old(self):
227 pass # don't repreat this test
229 @unittest.skipUnless(
230 os.name == 'posix',
231 'os.path.expanduser() does not use HOME on Windows (since 3.8)')
232 def test_get_standard_config_files(self):
233 os.environ['HOME'] = '/home/parrot'
234 # TODO: set up mock home directory under Windows
235 self.assertEqual(self.option_parser.get_standard_config_files(),
236 ['/etc/docutils.conf',
237 './docutils.conf',
238 '/home/parrot/.docutils'])
239 # split at ':', expand leading '~':
240 os.environ['DOCUTILSCONFIG'] = ('/etc/docutils2.conf'
241 ':~/.config/docutils.conf')
242 self.assertEqual(self.option_parser.get_standard_config_files(),
243 ['/etc/docutils2.conf',
244 '/home/parrot/.config/docutils.conf'])
247 class HelperFunctionsTests(unittest.TestCase):
249 pathdict = {'foo': 'hallo', 'ham': 'häm', 'spam': 'spam'}
250 keys = ['foo', 'ham']
252 def setUp(self):
253 with warnings.catch_warnings():
254 warnings.filterwarnings('ignore', category=DeprecationWarning)
255 self.option_parser = frontend.OptionParser(
256 components=(rst.Parser,), read_config_files=None)
258 def test_make_paths_absolute(self):
259 pathdict = self.pathdict.copy()
260 frontend.make_paths_absolute(pathdict, self.keys, base_path='base')
261 self.assertEqual(pathdict['foo'], os.path.abspath('base/hallo'))
262 self.assertEqual(pathdict['ham'], os.path.abspath('base/häm'))
263 # not touched, because key not in keys:
264 self.assertEqual(pathdict['spam'], 'spam')
266 def test_make_paths_absolute_cwd(self):
267 # With base_path None, the cwd is used as base path.
268 # Settings values may-be `unicode` instances, therefore
269 # os.getcwdu() is used and the converted path is a unicode instance:
270 pathdict = self.pathdict.copy()
271 frontend.make_paths_absolute(pathdict, self.keys)
272 self.assertEqual(pathdict['foo'], os.path.abspath('hallo'))
273 self.assertEqual(pathdict['ham'], os.path.abspath('häm'))
274 # not touched, because key not in keys:
275 self.assertEqual(pathdict['spam'], 'spam')
277 boolean_settings = (
278 (True, True),
279 ('1', True),
280 ('on', True),
281 ('yes', True),
282 ('true', True),
283 ('0', False),
284 ('off', False),
285 ('no', False),
286 ('false', False),
289 def test_validate_boolean(self):
290 for v, result in self.boolean_settings:
291 self.assertEqual(frontend.validate_boolean(v), result)
293 def test_validate_ternary(self):
294 tests = (
295 ('500V', '500V'),
296 ('parrot', 'parrot'),
298 for v, result in self.boolean_settings + tests:
299 self.assertEqual(frontend.validate_ternary(v), result)
301 def test_validate_threshold(self):
302 tests = (('1', 1),
303 ('info', 1),
304 ('warning', 2),
305 ('error', 3),
306 ('severe', 4),
307 ('none', 5),
309 for v, result in tests:
310 self.assertEqual(
311 frontend.validate_threshold(v), result)
312 with self.assertRaisesRegex(LookupError, "unknown threshold: 'debug'"):
313 frontend.validate_threshold('debug')
315 def test_validate_colon_separated_string_list(self):
316 tests = (('a', ['a']),
317 ('a:b', ['a', 'b']),
318 (['a'], ['a']),
319 (['a', 'b:c'], ['a', 'b', 'c']),
321 for v, result in tests:
322 self.assertEqual(
323 frontend.validate_colon_separated_string_list(v), result)
325 def test_validate_comma_separated_list(self):
326 tests = (('a', ['a']),
327 ('a,b', ['a', 'b']),
328 (['a'], ['a']),
329 (['a', 'b,c'], ['a', 'b', 'c']),
331 for v, result in tests:
332 self.assertEqual(frontend.validate_comma_separated_list(v), result)
334 def test_validate_math_output(self):
335 tests = (('', []),
336 ('LaTeX ', ['latex', '']),
337 ('MathML', ['mathml', '']),
338 ('MathML PanDoc', ['mathml', 'pandoc']),
339 ('HTML math.css, X.css', ['html', 'math.css, X.css']),
340 ('MathJax /MathJax.js', ['mathjax', '/MathJax.js']),
342 for v, result in tests:
343 self.assertEqual(frontend.validate_math_output(v), result)
345 def test_validate_math_output_errors(self):
346 tests = (('XML', 'Unknown math output format: "XML",\n'
347 " choose from ('html', 'latex', 'mathml', 'mathjax')."),
348 ('MathML blame', 'MathML converter "blame" not supported,\n'
349 " choose from ('', 'latexml', 'ttm', 'blahtexml', "
350 "'pandoc')."),
352 for value, message in tests:
353 with self.assertRaises(LookupError) as cm:
354 frontend.validate_math_output(value)
355 self.assertEqual(message, str(cm.exception))
357 def test_validate_url_trailing_slash(self):
358 tests = (('', './'),
359 (None, './'),
360 ('http://example.org', 'http://example.org/'),
361 ('http://example.org/', 'http://example.org/'),
363 for v, result in tests:
364 self.assertEqual(frontend.validate_url_trailing_slash(v), result)
366 def test_validate_smartquotes_locales(self):
367 tests = (
368 ('en:ssvv', [('en', 'ssvv')]),
369 ('sd:«»°°', [('sd', '«»°°')]),
370 ([('sd', '«»°°'), 'ds:°°«»'], [('sd', '«»°°'), ('ds', '°°«»')]),
371 ('frs:« : »:((:))', [('frs', ['« ', ' »', '((', '))'])]),
373 for v, result in tests:
374 self.assertEqual(frontend.validate_smartquotes_locales(v), result)
376 def test_set_conditions_deprecation_warning(self):
377 reporter = utils.Reporter('test', 1, 4)
378 with self.assertWarnsRegex(DeprecationWarning,
379 'Set attributes via configuration '):
380 reporter.set_conditions('foo', 1, 4) # trigger warning
383 if __name__ == '__main__':
384 unittest.main()