3 # Author: David Goodger <goodger@python.org>
4 # Copyright: This module has been placed in the public domain.
7 Tests of runtime settings.
13 from pathlib
import Path
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'))
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:
48 'old': {'datestamp': '%Y-%m-%d %H:%M UTC',
51 'python_home': 'http://www.python.org',
54 'stylesheet_path': ['stylesheets/pep.css'],
55 'template': fixpath('pep-html-template'),
57 'one': {'datestamp': '%Y-%m-%d %H:%M UTC',
60 'python_home': 'http://www.python.org',
62 'record_dependencies': utils
.DependencyList(),
65 'stylesheet_path': ['stylesheets/pep.css'],
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',
74 'record_dependencies': utils
.DependencyList(),
76 'stylesheet_path': ['test.css'],
77 'trim_footnote_reference_space': None,
78 'output_encoding_error_handler': 'namereplace',
81 # use defaults from html5_polyglot writer component
82 # ignore settings in [html4css1 writer] section,
85 'record_dependencies': utils
.DependencyList(),
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',
97 'list2': {'expose_internals': ['a', 'b', 'c', 'd', 'e', 'f'],
98 'smartquotes_locales': [('de', '«»‹›'),
101 ('fr', ['« ', ' »', '‹ ', ' ›'])
103 'strip_classes': ['spam', 'pan', 'fun', 'parrot',
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."""
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()
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
):
136 expected
.update(self
.settings
[name
])
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())
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(
160 'Error in config file ".*config_syntax_error.txt", '
161 r
'section "\[general\]"'):
162 self
.files_settings('syntax_error')
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'))
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.
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
__
224 os
.environ
= self
.orig_environ
227 pass # don't repreat this test
229 @unittest.skipUnless(
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',
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']
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')
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
):
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
):
309 for v
, result
in tests
:
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']),
319 (['a', 'b:c'], ['a', 'b', 'c']),
321 for v
, result
in tests
:
323 frontend
.validate_colon_separated_string_list(v
), result
)
325 def test_validate_comma_separated_list(self
):
326 tests
= (('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
):
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', "
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
):
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
):
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__':