4 # Author: Martin Blais <blais@furius.ca>
5 # Copyright: This module has been placed in the public domain.
8 Test the `Publisher` facade and the ``publish_*`` convenience functions.
11 from pathlib
import Path
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]))
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'
32 This is a test document with a broken reference: nonexistent_
34 pseudoxml_output
= """\
35 <document ids="test-document" names="test\\ document" source="<string>" title="Test Document">
39 This is a test document with a broken reference: \n\
40 <problematic ids="problematic-1" refid="system-message-1">
42 <section classes="system-messages">
44 Docutils System Messages
45 <system_message backrefs="problematic-1" ids="system-message-1" level="3" line="4" source="<string>" type="ERROR">
47 Unknown target name: "nonexistent".
49 exposed_pseudoxml_output
= """\
50 <document ids="test-document" internal:refnames="{'nonexistent': [<reference: <#text: 'nonexistent'>>]}" names="test\\ document" source="<string>" title="Test Document">
54 This is a test document with a broken reference: \n\
55 <problematic ids="problematic-1" refid="system-message-1">
57 <section classes="system-messages">
59 Docutils System Messages
60 <system_message backrefs="problematic-1" ids="system-message-1" level="3" line="4" source="<string>" type="ERROR">
62 Unknown target name: "nonexistent".
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
)
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
):
154 settings
= {'_disable_config': True,
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(),
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)
190 # Output is encoded according to "output_encoding" setting.
191 settings
= self
.settings |
{'input_encoding': 'utf-16',
192 'output_encoding': 'unicode'}
194 expected
= ('<document source="<string>">\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'
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."""
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'],
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(
279 settings_overrides
={'expose_internals':
280 ['refnames', 'do_not_expose'],
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',
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
,
299 parser
='restructuredtext',
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
))
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',
329 self
.assertEqual(pseudoxml_output
, output
)
332 if __name__
== '__main__':