Update calls to the `core.publish_*()` convenience functions.
[docutils.git] / docutils / test / test_publisher.py
blob34d13f0f16c4244890762a3177eebf61d8f8e8e8
1 #!/usr/bin/env python3
3 # $Id$
4 # Author: Martin Blais <blais@furius.ca>
5 # Copyright: This module has been placed in the public domain.
7 """
8 Test the `Publisher` facade and the ``publish_*`` convenience functions.
9 """
10 import pickle
11 from pathlib import Path
12 import sys
13 import unittest
15 if __name__ == '__main__':
16 # prepend the "docutils root" to the Python library path
17 # so we import the local `docutils` package.
18 sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
20 import docutils
21 from docutils import core, nodes, parsers, readers, writers
22 import docutils.parsers.null
24 # DATA_ROOT is ./test/data/ from the docutils root
25 DATA_ROOT = Path(__file__).parent / 'data'
28 test_document = """\
29 Test Document
30 =============
32 This is a test document with a broken reference: nonexistent_
33 """
34 pseudoxml_output = """\
35 <document ids="test-document" names="test\\ document" source="<string>" title="Test Document">
36 <title>
37 Test Document
38 <paragraph>
39 This is a test document with a broken reference: \n\
40 <problematic ids="problematic-1" refid="system-message-1">
41 nonexistent_
42 <section classes="system-messages">
43 <title>
44 Docutils System Messages
45 <system_message backrefs="problematic-1" ids="system-message-1" level="3" line="4" source="<string>" type="ERROR">
46 <paragraph>
47 Unknown target name: "nonexistent".
48 """
49 exposed_pseudoxml_output = """\
50 <document ids="test-document" internal:refnames="{'nonexistent': [<reference: <#text: 'nonexistent'>>]}" names="test\\ document" source="<string>" title="Test Document">
51 <title>
52 Test Document
53 <paragraph>
54 This is a test document with a broken reference: \n\
55 <problematic ids="problematic-1" refid="system-message-1">
56 nonexistent_
57 <section classes="system-messages">
58 <title>
59 Docutils System Messages
60 <system_message backrefs="problematic-1" ids="system-message-1" level="3" line="4" source="<string>" type="ERROR">
61 <paragraph>
62 Unknown target name: "nonexistent".
63 """
66 class PublisherTests(unittest.TestCase):
68 def test__init__(self):
69 reader = readers.standalone.Reader()
70 parser = parsers.null.Parser()
71 writer = writers.null.Writer()
72 # arguments may be component instances ...
73 publisher = core.Publisher(reader, parser, writer)
74 self.assertEqual(publisher.reader, reader)
75 self.assertEqual(publisher.parser, parser)
76 self.assertEqual(publisher.writer, writer)
77 # ... or names
78 publisher = core.Publisher('standalone', parser, writer)
79 self.assertTrue(isinstance(publisher.reader,
80 readers.standalone.Reader))
81 self.assertEqual(publisher.parser, parser)
82 self.assertEqual(publisher.writer, writer)
84 publisher = core.Publisher(reader, 'rst', writer)
85 self.assertEqual(publisher.reader, reader)
86 self.assertTrue(isinstance(publisher.parser, parsers.rst.Parser))
87 self.assertEqual(publisher.writer, writer)
89 publisher = core.Publisher(reader, parser, 'latex')
90 self.assertEqual(publisher.reader, reader)
91 self.assertEqual(publisher.parser, parser)
92 self.assertTrue(isinstance(publisher.writer, writers.latex2e.Writer))
94 def test_set_reader(self):
95 publisher = core.Publisher(parser='null')
96 parser = parsers.null.Parser()
97 # "parser" argument can be an instance or name
98 publisher.set_reader('standalone', parser='rst')
99 self.assertTrue(isinstance(publisher.parser, parsers.rst.Parser))
100 # synchronize parser attributes of publisher and reader:
101 self.assertEqual(publisher.reader.parser, publisher.parser)
102 # the "parser_name" argument is deprecated;
103 with self.assertWarnsRegex(DeprecationWarning,
104 'Argument "parser_name" will be removed'):
105 publisher.set_reader('standalone', parser=None, parser_name='rst')
106 self.assertTrue(isinstance(publisher.parser, parsers.rst.Parser))
107 self.assertEqual(publisher.reader.parser, publisher.parser)
108 # "parser" takes precedence
109 with self.assertWarns(DeprecationWarning):
110 publisher.set_reader('standalone', parser, parser_name='rst')
111 self.assertEqual(publisher.parser, parser)
112 self.assertEqual(publisher.reader.parser, publisher.parser)
113 # if there is no other parser specified, use self.parser
114 publisher.set_reader('standalone')
115 self.assertTrue(isinstance(publisher.parser, parsers.null.Parser))
116 self.assertEqual(publisher.reader.parser, publisher.parser)
118 def test_set_components(self):
119 publisher = core.Publisher()
120 reader = readers.standalone.Reader()
121 parser = parsers.null.Parser()
122 writer = writers.null.Writer()
123 # set components from names
124 with self.assertWarnsRegex(PendingDeprecationWarning,
125 'set_components.* will be removed'):
126 publisher.set_components('pep', 'rst', 'odt')
127 self.assertTrue(isinstance(publisher.reader, readers.pep.Reader))
128 self.assertTrue(isinstance(publisher.parser, parsers.rst.Parser))
129 self.assertTrue(isinstance(publisher.writer, writers.odf_odt.Writer))
130 # but don't overwrite registered component instances
131 publisher = core.Publisher(reader, parser, writer)
132 with self.assertWarns(PendingDeprecationWarning):
133 publisher.set_components('standalone', 'xml', 'odt')
134 self.assertEqual(publisher.reader, reader)
135 self.assertEqual(publisher.parser, parser)
136 self.assertEqual(publisher.writer, writer)
138 def test_set_destination(self):
139 # Exit if `_destination` and `output` settings conflict.
140 publisher = core.Publisher()
141 publisher.get_settings(output='out_name', _destination='out_name')
142 # no conflict if both have same value:
143 publisher.set_destination()
144 # no conflict if both are overridden:
145 publisher.set_destination(destination_path='winning_dest')
146 # ... also sets _destination to 'winning_dest' -> conflict
147 with self.assertRaises(SystemExit):
148 publisher.set_destination()
151 class ConvenienceFunctionTests(unittest.TestCase):
152 maxDiff = None
154 settings = {'_disable_config': True,
155 'datestamp': False}
157 def test_publish_cmdline(self):
158 # the "*_name" arguments will be removed
159 with self.assertWarns(PendingDeprecationWarning):
160 core.publish_cmdline(writer_name='null',
161 argv=[(DATA_ROOT/'include.txt').as_posix()],
162 settings_overrides={'traceback': True})
164 def test_input_error_handling(self):
165 # core.publish_cmdline(argv=['nonexisting/path'])
166 # exits with a short message, if `traceback` is False,
167 # pass IOErrors to calling application if `traceback` is True:
168 with self.assertRaises(IOError):
169 core.publish_cmdline(argv=['nonexisting/path'],
170 settings_overrides={'traceback': True})
172 def test_output_error_handling(self):
173 # pass IOErrors to calling application if `traceback` is True
174 with self.assertRaises(docutils.io.OutputError):
175 core.publish_cmdline(argv=[(DATA_ROOT/'include.txt').as_posix(),
176 'nonexisting/path'],
177 settings_overrides={'traceback': True})
179 def test_destination_output_conflict(self):
180 # Exit if positional argument and --output option conflict.
181 settings = {'output': 'out_name'}
182 with self.assertRaises(SystemExit):
183 core.publish_cmdline(argv=['-', 'dest_name'],
184 settings_overrides=settings)
186 def test_publish_string_input_encoding(self):
187 """Test handling of encoded input."""
188 # Transparently decode `bytes` source (with "input_encoding" setting)
189 # default: utf-8
190 # Output is encoded according to "output_encoding" setting.
191 settings = self.settings | {'input_encoding': 'utf-16',
192 'output_encoding': 'unicode'}
193 source = 'test → me'
194 expected = ('<document source="<string>">\n'
195 ' <paragraph>\n'
196 ' test → me\n')
197 output = core.publish_string(source.encode('utf-16'),
198 settings_overrides=settings)
199 self.assertEqual(expected, output)
201 # encoding declaration in source (used if input_encoding is None)
202 # input encoding detection will be removed in Docutils 1.0
203 source = '.. encoding: latin1\n\nGrüße'
204 settings['input_encoding'] = None
205 output = core.publish_string(source.encode('latin1'),
206 settings_overrides=settings)
207 self.assertTrue(output.endswith('Grüße\n'))
209 def test_publish_string_output_encoding(self):
210 settings = self.settings | {'output_encoding': 'latin1'}
211 settings['output_encoding_error_handler'] = 'replace'
212 source = 'Grüß → dich'
213 expected = ('<document source="<string>">\n'
214 ' <paragraph>\n'
215 ' Grüß → dich\n')
216 # encode output, return `bytes`
217 output = bytes(core.publish_string(source,
218 settings_overrides=settings))
219 self.assertEqual(expected.encode('latin1', 'replace'), output)
221 def test_publish_string_output_encoding_odt(self):
222 """The ODT writer generates a zip archive, not a `str`.
224 TODO: return `str` with document as "flat XML" (.fodt).
226 settings = self.settings | {'output_encoding': 'unicode',
227 'warning_stream': ''}
228 with self.assertRaisesRegex(docutils.utils.SystemMessage,
229 'The ODT writer returns `bytes` '):
230 core.publish_string('test', writer='odt',
231 settings_overrides=settings)
233 def test_publish_string_deprecation_warning(self):
234 """The "*_name" arguments are deprecated."""
235 source = 'test → me'
236 with self.assertWarns(PendingDeprecationWarning):
237 output = core.publish_string(source, writer_name='xml')
238 # ... but should still set the corresponding component:
239 self.assertTrue(output.decode('utf-8').startswith(
240 '<?xml version="1.0" encoding="utf-8"?>'))
243 class PublishDoctreeTestCase(unittest.TestCase, docutils.SettingsSpec):
245 settings_default_overrides = {
246 '_disable_config': True,
247 'warning_stream': docutils.io.NullOutput(),
248 'output_encoding': 'unicode'}
250 def test_publish_doctree(self):
251 # Test `publish_doctree` and `publish_from_doctree`.
253 # Produce the document tree.
254 with self.assertWarns(PendingDeprecationWarning):
255 doctree = core.publish_doctree(
256 source=test_document, reader='standalone',
257 parser_name='restructuredtext', settings_spec=self,
258 settings_overrides={'expose_internals':
259 ['refnames', 'do_not_expose'],
260 'report_level': 5})
261 self.assertTrue(isinstance(doctree, nodes.document))
263 # Confirm that transforms have been applied (in this case, the
264 # DocTitle transform):
265 self.assertTrue(isinstance(doctree[0], nodes.title))
266 self.assertTrue(isinstance(doctree[1], nodes.paragraph))
267 # Confirm that the Messages transform has not yet been applied:
268 self.assertEqual(2, len(doctree))
270 # The `do_not_expose` attribute may not show up in the
271 # pseudoxml output because the expose_internals transform may
272 # not be applied twice.
273 doctree.do_not_expose = 'test'
274 # Write out the document:
275 output = core.publish_from_doctree(
276 doctree,
277 writer='pseudoxml',
278 settings_spec=self,
279 settings_overrides={'expose_internals':
280 ['refnames', 'do_not_expose'],
281 'report_level': 1,
282 'output_encoding': 'unicode'})
283 self.assertEqual(exposed_pseudoxml_output, output)
285 # Test publishing parts using document as the source.
286 parts = core.publish_parts(
287 reader='doctree', source_class=docutils.io.DocTreeInput,
288 source=doctree, source_path='test', writer='html',
289 settings_spec=self)
290 self.assertTrue(isinstance(parts, dict))
292 def test_publish_pickle(self):
293 # Test publishing a document tree with pickling and unpickling.
295 # Produce the document tree.
296 doctree = core.publish_doctree(
297 source=test_document,
298 reader='standalone',
299 parser='restructuredtext',
300 settings_spec=self)
301 self.assertTrue(isinstance(doctree, nodes.document))
303 # Pickle the document. Note: if this fails, some unpickleable
304 # reference has been added somewhere within the document tree.
305 # If so, you need to fix that.
307 # Note: Please do not remove this test, this is an important
308 # requirement, applications will be built on the assumption
309 # that we can pickle the document.
311 # Remove the reporter and the transformer before pickling.
312 doctree.reporter = None
313 doctree.transformer = None
315 doctree_pickled = pickle.dumps(doctree)
316 self.assertTrue(isinstance(doctree_pickled, bytes))
317 del doctree
319 # Unpickle the document.
320 doctree_zombie = pickle.loads(doctree_pickled)
321 self.assertTrue(isinstance(doctree_zombie, nodes.document))
323 # Write out the document:
324 with self.assertWarnsRegex(PendingDeprecationWarning,
325 'Argument "writer_name" will be removed '):
326 output = core.publish_from_doctree(doctree_zombie,
327 writer_name='pseudoxml',
328 settings_spec=self)
329 self.assertEqual(pseudoxml_output, output)
332 if __name__ == '__main__':
333 unittest.main()